Juice pass: procedural SFX, particles, polished visuals

Sound (SfxManager.cs):
- Procedural audio synthesis via AudioStreamWav — no external files
- Distinct tones for place, produce, transfer, deliver, move, destroy, victory
- Simple ADSR envelope, sine/triangle waveforms, filtered noise for swooshes

Pieces (PieceView.cs):
- Warm earthy palette: sage green, deep teal, dusty rose, burnt sienna
- Drop shadow under each piece for depth
- 3-stop radial gradient (bright center → main → dark rim)
- Scale bounce on placement (0 → 1.15 → 1.0 with back-out easing)
- Cargo indicator pulses gently when carrying

Trajectories (TrajectView.cs):
- Arrowhead at endpoint showing movement direction
- Antialiased lines with piece-matched colors

Cells (CellView.cs):
- Warmer palette: parchment/walnut board, deep forest production, aged gold demand
- Production flash uses warm golden glow instead of white
- Subtle inner shadow for visual depth

Animations (EventAnimator.cs):
- Production: golden particles burst from production cells
- Transfer: cargo slides with 2-particle trail + back-out whip easing
- Destruction: pieces shrink + spin + red particle explosion
- Victory: 40 confetti particles rain across the screen
- All phases trigger appropriate SFX

UI polish:
- ControlBar: styled buttons with rounded corners, disabled states
- MetricsOverlay: fade-in + scale animation, sequential metric reveals
- ObjectivePanel: animated progress bars, styled fills, green flash on completion
- Main: fade-in/out transitions between level select and gameplay

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Bouchet 2026-04-10 23:05:55 +02:00
parent e1218b3eaa
commit 450c069854
11 changed files with 874 additions and 174 deletions

View file

@ -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. 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. **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.

107
PLAN_juice.md Normal file
View file

@ -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é

View file

@ -7,6 +7,7 @@ public partial class CellView : Node2D
{ {
private ColorRect _background = null!; private ColorRect _background = null!;
private ColorRect _highlight = null!; private ColorRect _highlight = null!;
private ColorRect _innerShadow = null!;
private Label _label = null!; private Label _label = null!;
// Hover outline (4 thin rects forming a border) // Hover outline (4 thin rects forming a border)
@ -17,13 +18,14 @@ public partial class CellView : Node2D
public Coords Coords { get; private set; } public Coords Coords { get; private set; }
private static readonly Color LightColor = new("#F0D9B5"); // Warmer, more grounded palette
private static readonly Color DarkColor = new("#B58863"); private static readonly Color LightColor = new("#E8D5A8"); // warm parchment
private static readonly Color WallColor = new("#555555"); private static readonly Color DarkColor = new("#A07850"); // warm walnut
private static readonly Color ProductionColor = new("#6B8E5A"); private static readonly Color WallColor = new("#3A3A3A"); // charcoal
private static readonly Color DemandColor = new("#C9A833"); 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 HighlightColor = new("#44FF4444");
private static readonly Color HoverOutlineColor = new("#FFFFFFAA"); private static readonly Color HoverOutlineColor = new("#FFFFFF88");
private const int OutlineWidth = 2; private const int OutlineWidth = 2;
@ -49,6 +51,17 @@ public partial class CellView : Node2D
}; };
AddChild(_background); 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 _highlight = new ColorRect
{ {
Size = new Vector2(cellSize, cellSize), Size = new Vector2(cellSize, cellSize),
@ -60,56 +73,36 @@ public partial class CellView : Node2D
AddChild(_highlight); AddChild(_highlight);
// Hover outline (4 border rects) // Hover outline (4 border rects)
_hoverTop = new ColorRect _hoverTop = CreateBorderRect(new Vector2(cellSize, OutlineWidth), Vector2.Zero);
{ _hoverBottom = CreateBorderRect(new Vector2(cellSize, OutlineWidth), new Vector2(0, cellSize - OutlineWidth));
Size = new Vector2(cellSize, OutlineWidth), _hoverLeft = CreateBorderRect(new Vector2(OutlineWidth, cellSize), Vector2.Zero);
Position = Vector2.Zero, _hoverRight = CreateBorderRect(new Vector2(OutlineWidth, cellSize), new Vector2(cellSize - OutlineWidth, 0));
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);
_label = new Label _label = new Label
{ {
Position = new Vector2(2, 2), Position = new Vector2(4, 3),
Text = "", Text = "",
MouseFilter = Control.MouseFilterEnum.Ignore 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); 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 SetLabel(string text) => _label.Text = text;
public void SetHighlight(bool on) => _highlight.Visible = on; public void SetHighlight(bool on) => _highlight.Visible = on;
@ -128,14 +121,15 @@ public partial class CellView : Node2D
} }
/// <summary> /// <summary>
/// Brief white flash on the cell to signal production. /// Production pulse: warm glow that radiates outward.
/// </summary> /// </summary>
public void FlashProduce(float duration = 0.3f) 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; _highlight.Visible = true;
var tween = CreateTween(); 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)); tween.TweenCallback(Callable.From(() => _highlight.Visible = false));
} }
} }

View file

@ -1,4 +1,5 @@
using Godot; using Godot;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Chessistics.Engine.Commands; using Chessistics.Engine.Commands;
@ -37,6 +38,7 @@ public partial class Main : Node2D
private PanelContainer _sidePanel = null!; private PanelContainer _sidePanel = null!;
private PanelContainer _controlBarWrapper = null!; private PanelContainer _controlBarWrapper = null!;
private Camera2D _camera = null!; private Camera2D _camera = null!;
private ColorRect _fadeOverlay = null!;
// Simulation timer // Simulation timer
private Godot.Timer _simTimer = null!; private Godot.Timer _simTimer = null!;
@ -58,6 +60,9 @@ public partial class Main : Node2D
BuildSceneTree(); BuildSceneTree();
ConnectSignals(); ConnectSignals();
ShowLevelSelect(); ShowLevelSelect();
// Fade in from black on startup
FadeIn(0.5f);
} }
public override void _UnhandledInput(InputEvent @event) 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() private void BuildSceneTree()
{ {
// Camera // Camera
_camera = new Camera2D { Enabled = true }; _camera = new Camera2D { Enabled = true };
AddChild(_camera); AddChild(_camera);
// SFX
var sfx = new SfxManager();
AddChild(sfx);
// Board // Board
_boardView = new BoardView(); _boardView = new BoardView();
AddChild(_boardView); AddChild(_boardView);
@ -204,6 +230,15 @@ public partial class Main : Node2D
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect); _levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_levelSelectScreen); 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 // Initialize animator
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay); _eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
} }
@ -254,8 +289,15 @@ public partial class Main : Node2D
private void OnLevelSelected(int levelIndex) private void OnLevelSelected(int levelIndex)
{ {
SfxManager.Instance?.PlayClick();
_currentLevelIndex = levelIndex; _currentLevelIndex = levelIndex;
// Fade out, load, fade in
FadeOut(0.25f, () =>
{
LoadLevel(levelIndex); LoadLevel(levelIndex);
FadeIn(0.3f);
});
} }
private void LoadLevel(int index) private void LoadLevel(int index)
@ -370,17 +412,13 @@ public partial class Main : Node2D
private void CreatePieceVisual(PiecePlacedEvent placed) private void CreatePieceVisual(PiecePlacedEvent placed)
{ {
SfxManager.Instance?.PlayPlace();
var pieceView = new PieceView(); var pieceView = new PieceView();
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView); pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
_boardView.AddChild(pieceView); _boardView.AddChild(pieceView);
var color = placed.Kind switch var color = PieceView.GetPieceColor(placed.Kind);
{
PieceKind.Rook => new Color("#4A7AB5"),
PieceKind.Bishop => new Color("#B54A8E"),
PieceKind.Knight => new Color("#B5824A"),
_ => Colors.White
};
var trajectView = new TrajectView(); var trajectView = new TrajectView();
trajectView.Setup(placed.PieceId, trajectView.Setup(placed.PieceId,

View file

@ -6,21 +6,25 @@ namespace Chessistics.Scripts.Pieces;
public partial class PieceView : Node2D public partial class PieceView : Node2D
{ {
private Sprite2D _shadow = null!;
private Sprite2D _sprite = null!; private Sprite2D _sprite = null!;
private ColorRect _cargoIndicator = null!; private ColorRect _cargoIndicator = null!;
private Label _label = null!; private Label _label = null!;
private Tween? _cargoPulseTween;
public int PieceId { get; private set; } public int PieceId { get; private set; }
public PieceKind Kind { get; private set; } public PieceKind Kind { get; private set; }
public Coords StartCell { get; private set; } public Coords StartCell { get; private set; }
public Coords EndCell { get; private set; } public Coords EndCell { get; private set; }
private static readonly Color PawnColor = new("#7AB54A"); // Muted, earthy palette — teal, sienna, gold tones
private static readonly Color RookColor = new("#4A7AB5"); private static readonly Color PawnColor = new("#5A8C6B"); // sage green
private static readonly Color BishopColor = new("#B54A8E"); private static readonly Color RookColor = new("#3D6B8E"); // deep teal
private static readonly Color KnightColor = new("#B5824A"); private static readonly Color BishopColor = new("#8E5A6B"); // dusty rose
private static readonly Color WoodCargoColor = new("#8B6914"); private static readonly Color KnightColor = new("#8E7A3D"); // burnt sienna
private static readonly Color StoneCargoColor = new("#808080"); 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) 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); Position = boardView.CoordsToPixel(startCell);
var color = kind switch var color = GetPieceColor(kind);
{
PieceKind.Pawn => PawnColor,
PieceKind.Rook => RookColor,
PieceKind.Bishop => BishopColor,
PieceKind.Knight => KnightColor,
_ => Colors.White
};
// Piece body (circle) // Shadow (slightly offset, rendered first)
_sprite = new Sprite2D(); _shadow = new Sprite2D();
var texture = new GradientTexture2D var shadowTex = new GradientTexture2D
{ {
Width = 48, Width = 44, Height = 44,
Height = 48,
Fill = GradientTexture2D.FillEnum.Radial, Fill = GradientTexture2D.FillEnum.Radial,
Gradient = new Gradient() Gradient = new Gradient()
}; };
texture.Gradient.SetColor(0, color); shadowTex.Gradient.SetColor(0, ShadowColor);
texture.Gradient.SetColor(1, color.Darkened(0.3f)); 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; _sprite.Texture = texture;
AddChild(_sprite); AddChild(_sprite);
@ -71,25 +85,46 @@ public partial class PieceView : Node2D
MouseFilter = Control.MouseFilterEnum.Ignore MouseFilter = Control.MouseFilterEnum.Ignore
}; };
_label.AddThemeFontSizeOverride("font_size", 16); _label.AddThemeFontSizeOverride("font_size", 16);
_label.AddThemeColorOverride("font_color", Colors.White); _label.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.9f));
AddChild(_label); AddChild(_label);
// Cargo indicator (hidden by default) // Cargo indicator (hidden by default)
_cargoIndicator = new ColorRect _cargoIndicator = new ColorRect
{ {
Size = new Vector2(14, 14), Size = new Vector2(12, 12),
Position = new Vector2(-7, -30), Position = new Vector2(-6, -28),
Visible = false, Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore MouseFilter = Control.MouseFilterEnum.Ignore
}; };
AddChild(_cargoIndicator); 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) public void SetCargo(CargoType? cargo)
{ {
_cargoPulseTween?.Kill();
_cargoPulseTween = null;
if (cargo == null) if (cargo == null)
{ {
_cargoIndicator.Visible = false; _cargoIndicator.Visible = false;
_cargoIndicator.Scale = Vector2.One;
return; return;
} }
@ -100,6 +135,16 @@ public partial class PieceView : Node2D
CargoType.Stone => StoneCargoColor, CargoType.Stone => StoneCargoColor,
_ => Colors.White _ => 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) public void AnimateMoveTo(Vector2 target, float duration = 0.3f)
@ -107,7 +152,6 @@ public partial class PieceView : Node2D
var tween = CreateTween(); var tween = CreateTween();
if (Kind == PieceKind.Knight) if (Kind == PieceKind.Knight)
{ {
// Arc animation for knight
var mid = (Position + target) / 2 + new Vector2(0, -30); var mid = (Position + target) / 2 + new Vector2(0, -30);
tween.TweenMethod(Callable.From<float>(t => tween.TweenMethod(Callable.From<float>(t =>
{ {
@ -119,7 +163,8 @@ public partial class PieceView : Node2D
} }
else else
{ {
tween.TweenProperty(this, "position", target, duration); tween.TweenProperty(this, "position", target, duration)
.SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine);
} }
} }
} }

View file

@ -5,15 +5,34 @@ namespace Chessistics.Scripts.Pieces;
public partial class TrajectView : Line2D public partial class TrajectView : Line2D
{ {
public int PieceId { get; private set; } public int PieceId { get; private set; }
private Polygon2D? _arrow;
public void Setup(int pieceId, Vector2 from, Vector2 to, Color color) public void Setup(int pieceId, Vector2 from, Vector2 to, Color color)
{ {
PieceId = pieceId; PieceId = pieceId;
Width = 3f; Width = 2.5f;
DefaultColor = new Color(color, 0.5f); DefaultColor = new Color(color, 0.35f);
Antialiased = true;
ClearPoints(); ClearPoints();
AddPoint(from); AddPoint(from);
AddPoint(to); AddPoint(to);
ZIndex = -1; 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);
} }
} }

View file

@ -23,14 +23,14 @@ public partial class EventAnimator : Node
private bool _animating; private bool _animating;
public bool IsAnimating => _animating; public bool IsAnimating => _animating;
private static readonly Color WoodCargoColor = new("#8B6914"); private static readonly Color WoodCargoColor = new("#A67C32");
private static readonly Color StoneCargoColor = new("#808080"); private static readonly Color StoneCargoColor = new("#7A7A7A");
private const float ProduceDuration = 0.3f; private const float ProduceDuration = 0.35f;
private const float TransferDuration = 0.25f; private const float TransferDuration = 0.28f;
private const float MoveDuration = 0.3f; private const float MoveDuration = 0.32f;
private const float KnightMoveDuration = 0.4f; private const float KnightMoveDuration = 0.42f;
private const float DestroyDuration = 0.3f; private const float DestroyDuration = 0.45f;
[Signal] [Signal]
public delegate void TurnAnimationCompletedEventHandler(); public delegate void TurnAnimationCompletedEventHandler();
@ -107,6 +107,8 @@ public partial class EventAnimator : Node
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
SfxManager.Instance?.PlayVictory();
SpawnConfetti();
_metricsOverlay.ShowMetrics(victory.Metrics); _metricsOverlay.ShowMetrics(victory.Metrics);
EmitSignal(SignalName.VictoryReached); EmitSignal(SignalName.VictoryReached);
})); }));
@ -137,45 +139,50 @@ public partial class EventAnimator : Node
List<PieceMovedEvent> moveEvents, List<PieceMovedEvent> moveEvents,
List<PieceDestroyedEvent> collisionEvents) List<PieceDestroyedEvent> collisionEvents)
{ {
// Phase 1: Produce — flash production cells // Phase 1: Produce — warm golden flash + particle burst
if (produceEvents.Count > 0) if (produceEvents.Count > 0)
{ {
var captured = produceEvents.ToList();
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
foreach (var evt in produceEvents.ToList()) SfxManager.Instance?.PlayProduce();
foreach (var evt in captured)
{ {
var cell = _boardView.GetCellView(evt.ProductionCell); var cell = _boardView.GetCellView(evt.ProductionCell);
cell?.FlashProduce(ProduceDuration); cell?.FlashProduce(ProduceDuration);
SpawnProduceParticles(evt.ProductionCell, evt.Type);
} }
})); }));
tween.TweenInterval(ProduceDuration); tween.TweenInterval(ProduceDuration);
produceEvents.Clear(); produceEvents.Clear();
} }
// Phase 2: Transfers — animate cargo sliding from giver to receiver // Phase 2: Transfers — cargo slides with trail particles
if (transferEvents.Count > 0) if (transferEvents.Count > 0)
{ {
// Capture the events list before clearing
var eventsToAnimate = transferEvents.ToList(); var eventsToAnimate = transferEvents.ToList();
// Step 1: remove cargo from givers + spawn sliding cargo sprites
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
bool hasDelivery = false;
foreach (var evt in eventsToAnimate) foreach (var evt in eventsToAnimate)
{ {
if (evt is CargoTransferredEvent transfer) if (evt is CargoTransferredEvent transfer)
{ {
// Remove cargo indicator from giver
if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value)) if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value))
_pieceViews[transfer.GivingPieceId.Value].SetCargo(null); _pieceViews[transfer.GivingPieceId.Value].SetCargo(null);
// Create sliding cargo sprite
SpawnCargoSlide(transfer); 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.TweenInterval(TransferDuration);
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
@ -196,9 +203,10 @@ public partial class EventAnimator : Node
transferEvents.Clear(); transferEvents.Clear();
} }
// Phase 3: Movement — all pieces move simultaneously // Phase 3: Movement — simultaneous, with sfx
if (moveEvents.Count > 0) if (moveEvents.Count > 0)
{ {
tween.TweenCallback(Callable.From(() => SfxManager.Instance?.PlayMove()));
tween.SetParallel(true); tween.SetParallel(true);
foreach (var moved in moveEvents) foreach (var moved in moveEvents)
{ {
@ -206,74 +214,218 @@ public partial class EventAnimator : Node
{ {
var target = _boardView.CoordsToPixel(moved.To); var target = _boardView.CoordsToPixel(moved.To);
float duration = pv.Kind == PieceKind.Knight ? KnightMoveDuration : MoveDuration; 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); tween.SetParallel(false);
moveEvents.Clear(); moveEvents.Clear();
} }
// Phase 4: Collision/Destruction // Phase 4: Collision/Destruction — shrink + spin + particles
if (collisionEvents.Count > 0) if (collisionEvents.Count > 0)
{ {
tween.SetParallel(true); var captured = collisionEvents.ToList();
foreach (var destroyed in collisionEvents)
{
var pieceId = destroyed.PieceId;
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
FlashPiece(pieceId); SfxManager.Instance?.PlayDestroy();
UnregisterPiece(pieceId); foreach (var destroyed in captured)
})); {
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.SetParallel(false); }
}));
tween.TweenInterval(DestroyDuration); tween.TweenInterval(DestroyDuration);
tween.TweenCallback(Callable.From(() =>
{
foreach (var destroyed in captured)
UnregisterPiece(destroyed.PieceId);
}));
collisionEvents.Clear(); collisionEvents.Clear();
} }
} }
/// <summary> // --- Visual Effects ---
/// Creates a temporary colored square that slides from the giver to the receiver.
/// </summary>
private void SpawnCargoSlide(CargoTransferredEvent transfer) private void SpawnCargoSlide(CargoTransferredEvent transfer)
{ {
var from = _boardView.CoordsToPixel(transfer.From); var from = _boardView.CoordsToPixel(transfer.From);
var to = _boardView.CoordsToPixel(transfer.To); var to = _boardView.CoordsToPixel(transfer.To);
var color = transfer.Type switch var color = GetCargoColor(transfer.Type);
var container = new Node2D { Position = from };
_boardView.AddChild(container);
// Main cargo square
var main = new ColorRect
{
Size = new Vector2(12, 12),
Position = new Vector2(-6, -6),
Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore
};
container.AddChild(main);
// Trail particles (2 smaller squares that follow with delay)
for (int i = 1; i <= 2; i++)
{
float trailDelay = i * 0.04f;
float trailSize = 12f - i * 3f;
float trailAlpha = 0.6f - i * 0.2f;
var trail = new ColorRect
{
Size = new Vector2(trailSize, trailSize),
Position = new Vector2(-trailSize / 2f, -trailSize / 2f),
Color = new Color(color, trailAlpha),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var trailContainer = new Node2D { Position = from };
trailContainer.AddChild(trail);
_boardView.AddChild(trailContainer);
var trailTween = trailContainer.CreateTween();
trailTween.TweenInterval(trailDelay);
trailTween.TweenProperty(trailContainer, "position", to, TransferDuration - trailDelay)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
trailTween.TweenCallback(Callable.From(() => trailContainer.QueueFree()));
}
// Main slide with whip easing
var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
}
private void SpawnProduceParticles(Coords cell, CargoType type)
{
var center = _boardView.CoordsToPixel(cell);
var color = GetCargoColor(type);
var rng = new Random();
for (int i = 0; i < 6; i++)
{
float angle = i * Mathf.Pi * 2f / 6f + (float)rng.NextDouble() * 0.5f;
float dist = 25f + (float)rng.NextDouble() * 15f;
float size = 4f + (float)rng.NextDouble() * 4f;
var particle = new ColorRect
{
Size = new Vector2(size, size),
Position = new Vector2(-size / 2f, -size / 2f),
Color = new Color(color, 0.8f),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var pNode = new Node2D { Position = center };
pNode.AddChild(particle);
_boardView.AddChild(pNode);
var target = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
var pt = pNode.CreateTween();
pt.SetParallel(true);
pt.TweenProperty(pNode, "position", target, ProduceDuration * 0.8f)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
pt.TweenProperty(pNode, "modulate:a", 0f, ProduceDuration);
pt.SetParallel(false);
pt.TweenCallback(Callable.From(() => pNode.QueueFree()));
}
}
private void SpawnDestroyParticles(Vector2 position)
{
var rng = new Random();
var red = new Color(0.9f, 0.25f, 0.2f);
for (int i = 0; i < 10; i++)
{
float angle = (float)rng.NextDouble() * Mathf.Pi * 2f;
float dist = 20f + (float)rng.NextDouble() * 30f;
float size = 3f + (float)rng.NextDouble() * 5f;
var particle = new ColorRect
{
Size = new Vector2(size, size),
Position = new Vector2(-size / 2f, -size / 2f),
Color = new Color(red, 0.9f),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var pNode = new Node2D { Position = position };
pNode.AddChild(particle);
_boardView.AddChild(pNode);
var target = position + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
var pt = pNode.CreateTween();
pt.SetParallel(true);
pt.TweenProperty(pNode, "position", target, DestroyDuration * 0.7f)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
pt.TweenProperty(pNode, "modulate:a", 0f, DestroyDuration);
pt.TweenProperty(pNode, "scale", Vector2.Zero, DestroyDuration)
.SetEase(Tween.EaseType.In);
pt.SetParallel(false);
pt.TweenCallback(Callable.From(() => pNode.QueueFree()));
}
}
private void SpawnConfetti()
{
var rng = new Random();
var viewport = GetViewport().GetVisibleRect().Size;
Color[] confettiColors = [
new("#FFD700"), new("#FF6B35"), new("#3D8E5A"),
new("#4A7AB5"), new("#B54A8E"), new("#E8D5A8")
];
for (int i = 0; i < 40; i++)
{
float x = (float)rng.NextDouble() * viewport.X;
float startY = -20f;
float endY = viewport.Y + 50f;
float size = 4f + (float)rng.NextDouble() * 6f;
float duration = 1.5f + (float)rng.NextDouble() * 1.5f;
float delay = (float)rng.NextDouble() * 0.5f;
float drift = ((float)rng.NextDouble() - 0.5f) * 80f;
var confetti = new ColorRect
{
Size = new Vector2(size, size * 0.5f),
Color = confettiColors[rng.Next(confettiColors.Length)],
MouseFilter = Control.MouseFilterEnum.Ignore
};
// Confetti needs to be in screen space (CanvasLayer)
var parent = GetTree().Root;
var cNode = new Node2D { Position = new Vector2(x, startY) };
cNode.AddChild(confetti);
parent.AddChild(cNode);
var ct = cNode.CreateTween();
ct.TweenInterval(delay);
ct.SetParallel(true);
ct.TweenProperty(cNode, "position", new Vector2(x + drift, endY), duration)
.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Sine);
ct.TweenProperty(cNode, "rotation", (float)rng.NextDouble() * Mathf.Pi * 4f, duration);
ct.SetParallel(false);
ct.TweenCallback(Callable.From(() => cNode.QueueFree()));
}
}
private static Color GetCargoColor(CargoType type) => type switch
{ {
CargoType.Wood => WoodCargoColor, CargoType.Wood => WoodCargoColor,
CargoType.Stone => StoneCargoColor, CargoType.Stone => StoneCargoColor,
_ => Colors.White _ => Colors.White
}; };
var sprite = new ColorRect
{
Size = new Vector2(14, 14),
Position = new Vector2(-7, -7),
Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore
};
var container = new Node2D { Position = from };
container.AddChild(sprite);
_boardView.AddChild(container);
var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Cubic);
slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
}
private void FlashPiece(int pieceId)
{
if (!_pieceViews.TryGetValue(pieceId, out var pv)) return;
var tween = pv.CreateTween();
tween.TweenProperty(pv, "modulate", new Color(1, 0.2f, 0.2f), 0.1f);
tween.TweenProperty(pv, "modulate", Colors.White, 0.1f);
tween.SetLoops(3);
}
public void ResetPiecePositions(BoardSnapshot snapshot) public void ResetPiecePositions(BoardSnapshot snapshot)
{ {
foreach (var ps in snapshot.Pieces) foreach (var ps in snapshot.Pieces)

View file

@ -0,0 +1,160 @@
using Godot;
using System;
namespace Chessistics.Scripts.Presentation;
/// <summary>
/// Procedural sound effects via synthesized waveforms.
/// No external audio files needed — everything is generated in code.
/// </summary>
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;
}
/// <summary>Simple ADSR-ish envelope: quick attack, sustain, smooth release.</summary>
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;
}
}

View file

@ -24,38 +24,92 @@ public partial class ControlBar : HBoxContainer
private OptionButton _speedSelect = null!; private OptionButton _speedSelect = null!;
private Label _turnLabel = 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() public override void _Ready()
{ {
_playButton = new Button { Text = "▶ PLAY" }; AddThemeConstantOverride("separation", 8);
_playButton = CreateStyledButton("PLAY");
_playButton.Pressed += () => EmitSignal(SignalName.PlayPressed); _playButton.Pressed += () => EmitSignal(SignalName.PlayPressed);
AddChild(_playButton); AddChild(_playButton);
_pauseButton = new Button { Text = "⏸ PAUSE" }; _pauseButton = CreateStyledButton("PAUSE");
_pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed); _pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed);
AddChild(_pauseButton); AddChild(_pauseButton);
_stepButton = new Button { Text = "⏭ STEP" }; _stepButton = CreateStyledButton("STEP");
_stepButton.Pressed += () => EmitSignal(SignalName.StepPressed); _stepButton.Pressed += () => EmitSignal(SignalName.StepPressed);
AddChild(_stepButton); AddChild(_stepButton);
_stopButton = new Button { Text = "⏹ STOP" }; _stopButton = CreateStyledButton("STOP");
_stopButton.Pressed += () => EmitSignal(SignalName.StopPressed); _stopButton.Pressed += () => EmitSignal(SignalName.StopPressed);
AddChild(_stopButton); 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("x1", 0);
_speedSelect.AddItem("x2", 1); _speedSelect.AddItem("x2", 1);
_speedSelect.AddItem("x4", 2); _speedSelect.AddItem("x4", 2);
_speedSelect.ItemSelected += OnSpeedSelected; _speedSelect.ItemSelected += OnSpeedSelected;
AddChild(_speedSelect); AddChild(_speedSelect);
// Spacer
AddChild(new Control { SizeFlagsHorizontal = SizeFlags.ExpandFill });
_turnLabel = new Label { Text = "Coup: --" }; _turnLabel = new Label { Text = "Coup: --" };
_turnLabel.AddThemeFontSizeOverride("font_size", 14); _turnLabel.AddThemeFontSizeOverride("font_size", 13);
_turnLabel.AddThemeColorOverride("font_color", new Color("#999999"));
AddChild(_turnLabel); AddChild(_turnLabel);
UpdateForPhase(SimPhase.Edit); 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) private void OnSpeedSelected(long index)
{ {
float speed = index switch float speed = index switch

View file

@ -10,46 +10,62 @@ public partial class MetricsOverlay : PanelContainer
[Signal] [Signal]
public delegate void RetryPressedEventHandler(); 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() public override void _Ready()
{ {
var vbox = new VBoxContainer(); var style = new StyleBoxFlat
vbox.SetAnchorsPreset(LayoutPreset.Center); {
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 !", Text = "VICTOIRE !",
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
title.AddThemeFontSizeOverride("font_size", 24); _titleLabel.AddThemeFontSizeOverride("font_size", 26);
title.AddThemeColorOverride("font_color", new Color("#FFD700")); _titleLabel.AddThemeColorOverride("font_color", new Color("#FFD700"));
vbox.AddChild(title); vbox.AddChild(_titleLabel);
vbox.AddChild(new HSeparator()); vbox.AddChild(new HSeparator());
_metricsLabel = new Label _piecesLabel = CreateMetricLabel();
{ vbox.AddChild(_piecesLabel);
Text = "", _turnsLabel = CreateMetricLabel();
HorizontalAlignment = HorizontalAlignment.Center vbox.AddChild(_turnsLabel);
}; _cellsLabel = CreateMetricLabel();
_metricsLabel.AddThemeFontSizeOverride("font_size", 14); vbox.AddChild(_cellsLabel);
vbox.AddChild(_metricsLabel);
vbox.AddChild(new HSeparator()); vbox.AddChild(new HSeparator());
var buttons = new HBoxContainer(); _buttons = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.Center };
buttons.Alignment = BoxContainer.AlignmentMode.Center; _buttons.AddThemeConstantOverride("separation", 16);
var retryBtn = new Button { Text = "Rejouer" }; var retryBtn = CreateStyledButton("Rejouer");
retryBtn.Pressed += () => EmitSignal(SignalName.RetryPressed); 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); nextBtn.Pressed += () => EmitSignal(SignalName.NextLevelPressed);
buttons.AddChild(nextBtn); _buttons.AddChild(nextBtn);
vbox.AddChild(buttons); vbox.AddChild(_buttons);
AddChild(vbox); AddChild(vbox);
Visible = false; Visible = false;
@ -57,11 +73,87 @@ public partial class MetricsOverlay : PanelContainer
public void ShowMetrics(Metrics metrics) public void ShowMetrics(Metrics metrics)
{ {
_metricsLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}\n" + _piecesLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}";
$"Coups: {metrics.TurnsTaken}\n" + _turnsLabel.Text = $"Coups: {metrics.TurnsTaken}";
$"Cases occupees: {metrics.CellsOccupied}"; _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; 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;
}
} }

View file

@ -6,7 +6,7 @@ namespace Chessistics.Scripts.UI;
public partial class ObjectivePanel : VBoxContainer public partial class ObjectivePanel : VBoxContainer
{ {
private readonly Dictionary<Coords, (Label label, ProgressBar bar)> _entries = new(); private readonly Dictionary<Coords, (Label label, ProgressBar bar, Label deadline)> _entries = new();
public void Setup(IReadOnlyList<DemandDef> demands) public void Setup(IReadOnlyList<DemandDef> demands)
{ {
@ -16,7 +16,7 @@ public partial class ObjectivePanel : VBoxContainer
var title = new Label { Text = "OBJECTIFS" }; var title = new Label { Text = "OBJECTIFS" };
title.AddThemeFontSizeOverride("font_size", 16); title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#FFD700")); title.AddThemeColorOverride("font_color", new Color("#B8942A")); // aged gold
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());
@ -24,9 +24,11 @@ public partial class ObjectivePanel : VBoxContainer
foreach (var demand in demands) foreach (var demand in demands)
{ {
var vbox = new VBoxContainer(); var vbox = new VBoxContainer();
vbox.AddThemeConstantOverride("separation", 2);
var label = new Label { Text = $"{demand.Name}: 0/{demand.Amount} {demand.Cargo}" }; var label = new Label { Text = $"{demand.Name}: 0/{demand.Amount} {demand.Cargo}" };
label.AddThemeFontSizeOverride("font_size", 12); label.AddThemeFontSizeOverride("font_size", 12);
label.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
vbox.AddChild(label); vbox.AddChild(label);
var bar = new ProgressBar var bar = new ProgressBar
@ -34,18 +36,34 @@ public partial class ObjectivePanel : VBoxContainer
MinValue = 0, MinValue = 0,
MaxValue = demand.Amount, MaxValue = demand.Amount,
Value = 0, Value = 0,
CustomMinimumSize = new Vector2(180, 16), CustomMinimumSize = new Vector2(180, 14),
ShowPercentage = false 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); vbox.AddChild(bar);
var deadline = new Label { Text = $"Deadline: {demand.Deadline} coups" }; var deadline = new Label { Text = $"Deadline: {demand.Deadline} coups" };
deadline.AddThemeFontSizeOverride("font_size", 10); deadline.AddThemeFontSizeOverride("font_size", 10);
deadline.AddThemeColorOverride("font_color", new Color("#AAAAAA")); deadline.AddThemeColorOverride("font_color", new Color("#777777"));
vbox.AddChild(deadline); vbox.AddChild(deadline);
AddChild(vbox); 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; if (!_entries.TryGetValue(demandCell, out var entry)) return;
entry.label.Text = $"{name}: {current}/{required}"; 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) 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);
}
} }
} }