Compare commits

...

6 commits

Author SHA1 Message Date
358ab48d59 Remove temp plan file, normalize GDD whitespace
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:42:07 +02:00
84ab0a1d0f Phase 7: Scroll wheel zoom + camera polish
- Mouse wheel up/down zooms camera in/out
- Zoom clamped between 0.3x and 3x to prevent getting lost
- Updated PLAN.md: marked phases 5-6 as done, added phase 7-8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:25:09 +02:00
8918140114 Phase 5: Dame (Queen) piece and network levels 7-8
Dame (Queen):
- Moves 1-2 cells in all 8 directions (orthogonal + diagonal)
- Social status 7 (highest — priority over all other pieces)
- Deep burgundy color, letter "D"
- Rare and powerful, forces strategic placement choices

Levels:
- Level 7 "La Dame Blanche": 10x10, walled arena with central fortress,
  1 queen available as a logistics superweapon
- Level 8 "Le Grand Reseau": 12x10, 4 productions + 4 demands,
  two vertical wall corridors, 2 queens, full network challenge

GDD updated with Dame section and status hierarchy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:24:14 +02:00
210be72100 Fix stop reset, piece selection visuals, add trajectory preview and menu button
Bugfixes:
- Stop now fully rebuilds piece visuals (fixes destroyed pieces not
  reappearing and lingering cargo indicators)
- Piece stock buttons use explicit selected/unselected styling instead
  of ambiguous toggle behavior (teal bg + bright border when selected)
- Color dots next to stock buttons match piece colors

Features:
- Clicking a placed piece highlights its start/end cells with piece color
- Back-to-menu button ("← Menu") in top-left returns to level selector
  with fade transition
- DetailPanel shows "Pion" name and has styled remove button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:21:38 +02:00
43a3e97f28 Add Godot uid for SfxManager
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:11:19 +02:00
450c069854 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>
2026-04-10 23:05:55 +02:00
22 changed files with 1087 additions and 235 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.

28
Data/levels/level_07.json Normal file
View file

@ -0,0 +1,28 @@
{
"id": 7,
"name": "La Dame Blanche",
"description": "La Dame entre en jeu. Sa portee sur 8 directions en fait une piece logistique supreme.",
"width": 10,
"height": 10,
"productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" },
{ "col": 9, "row": 0, "name": "Carriere", "cargo": "stone" }
],
"demands": [
{ "col": 9, "row": 9, "name": "Chateau", "cargo": "wood", "amount": 4, "deadline": 50 },
{ "col": 0, "row": 9, "name": "Forge Royale", "cargo": "stone", "amount": 4, "deadline": 50 }
],
"walls": [
{ "col": 3, "row": 3 }, { "col": 3, "row": 4 }, { "col": 3, "row": 5 }, { "col": 3, "row": 6 },
{ "col": 6, "row": 3 }, { "col": 6, "row": 4 }, { "col": 6, "row": 5 }, { "col": 6, "row": 6 },
{ "col": 4, "row": 6 }, { "col": 5, "row": 6 },
{ "col": 4, "row": 3 }, { "col": 5, "row": 3 }
],
"stock": [
{ "kind": "pawn", "count": 10 },
{ "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 },
{ "kind": "knight", "count": 2 },
{ "kind": "queen", "count": 1 }
]
}

33
Data/levels/level_08.json Normal file
View file

@ -0,0 +1,33 @@
{
"id": 8,
"name": "Le Grand Reseau",
"description": "Quatre productions, quatre demandes. Construisez un reseau logistique complet a travers un terrain hostile.",
"width": 12,
"height": 10,
"productions": [
{ "col": 0, "row": 0, "name": "Scierie Ouest", "cargo": "wood" },
{ "col": 11, "row": 0, "name": "Carriere Sud", "cargo": "stone" },
{ "col": 0, "row": 9, "name": "Scierie Nord", "cargo": "wood" },
{ "col": 11, "row": 9, "name": "Carriere Nord", "cargo": "stone" }
],
"demands": [
{ "col": 5, "row": 0, "name": "Depot Sud", "cargo": "wood", "amount": 3, "deadline": 60 },
{ "col": 6, "row": 9, "name": "Depot Nord", "cargo": "stone", "amount": 3, "deadline": 60 },
{ "col": 5, "row": 5, "name": "Forge Centrale", "cargo": "stone", "amount": 4, "deadline": 60 },
{ "col": 6, "row": 4, "name": "Chantier Central", "cargo": "wood", "amount": 4, "deadline": 60 }
],
"walls": [
{ "col": 3, "row": 2 }, { "col": 3, "row": 3 }, { "col": 3, "row": 4 },
{ "col": 3, "row": 5 }, { "col": 3, "row": 6 }, { "col": 3, "row": 7 },
{ "col": 8, "row": 2 }, { "col": 8, "row": 3 }, { "col": 8, "row": 4 },
{ "col": 8, "row": 5 }, { "col": 8, "row": 6 }, { "col": 8, "row": 7 },
{ "col": 4, "row": 4 }, { "col": 7, "row": 5 }
],
"stock": [
{ "kind": "pawn", "count": 16 },
{ "kind": "rook", "count": 6 },
{ "kind": "bishop", "count": 3 },
{ "kind": "knight", "count": 4 },
{ "kind": "queen", "count": 2 }
]
}

34
PLAN.md
View file

@ -28,22 +28,28 @@
- Production interval removed: all productions fire every turn - Production interval removed: all productions fire every turn
- GDD updated with Pion, 6 levels - GDD updated with Pion, 6 levels
## Phase 5: Network levels and Dame (Queen) ## Phase 5: Dame, network levels, juice pass (DONE)
**Goal**: Open-ended logistics puzzles with interconnected supply networks. - Dame (Queen): 8 directions, range 2, status 7 (highest)
- Levels 7-8: La Dame Blanche (10x10), Le Grand Reseau (12x10)
- Procedural SFX, particles, polished animations, fade transitions
- UI bugfixes: stop reset, piece selection visuals, back-to-menu button
- Trajectory preview on piece click
- Multiple productions feeding multiple demands through shared infrastructure. ## Phase 6: Godot integration (DONE)
- Dame piece: combines Rook + Bishop movement (range 2, all 8 directions).
- Powerful but expensive — forces cost/benefit tradeoffs.
- Larger boards (12x12+) with complex wall configurations.
- Potential for player-designed levels (level editor data format).
## Phase 6: Godot integration - Board renderer, piece placement, step/play/pause controls
- Event visualization with simultaneous animations per phase
- Victory/defeat screens with animated metrics
- Production flash, cargo slide trails, destruction particles, confetti
**Goal**: Playable visual prototype. ## Phase 7: Zoom, scroll wheel, and camera polish
- Board renderer: grid, walls, buildings, pieces. - Mouse scroll wheel to zoom in/out on board
- Drag-and-drop piece placement during Edit phase. - Zoom limits (min/max) to prevent getting lost
- Step/play/pause simulation controls. - Double-click to center on a piece
- Event visualization: cargo movement, transfers, delivery animations.
- Victory/defeat screens with Metrics display. ## Phase 8: Level editor (future)
- Player-designed levels via JSON export
- In-game editing of board size, walls, productions, demands, stock

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!;
@ -44,7 +46,7 @@ public partial class Main : Node2D
private bool _running; private bool _running;
private bool _panning; private bool _panning;
private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json", "level_04.json", "level_05.json", "level_06.json"]; private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json", "level_04.json", "level_05.json", "level_06.json", "level_07.json", "level_08.json"];
private const float SidePanelWidth = 280f; private const float SidePanelWidth = 280f;
private const float ControlBarHeight = 48f; private const float ControlBarHeight = 48f;
@ -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)
@ -66,6 +71,10 @@ public partial class Main : Node2D
{ {
if (mb.ButtonIndex == MouseButton.Middle) if (mb.ButtonIndex == MouseButton.Middle)
_panning = mb.Pressed; _panning = mb.Pressed;
else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelUp)
ZoomCamera(1.1f);
else if (mb.Pressed && mb.ButtonIndex == MouseButton.WheelDown)
ZoomCamera(0.9f);
} }
else if (@event is InputEventMouseMotion motion && _panning) else if (@event is InputEventMouseMotion motion && _panning)
{ {
@ -73,12 +82,40 @@ public partial class Main : Node2D
} }
} }
private void ZoomCamera(float factor)
{
var newZoom = _camera.Zoom * factor;
newZoom = newZoom.Clamp(new Vector2(0.3f, 0.3f), new Vector2(3f, 3f));
_camera.Zoom = newZoom;
}
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);
@ -107,14 +144,35 @@ public partial class Main : Node2D
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore; uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore;
_uiLayer.AddChild(uiRoot); _uiLayer.AddChild(uiRoot);
// Level title (top-left) // Level title bar (top-left)
var titleBar = new HBoxContainer();
titleBar.SetAnchorsPreset(Control.LayoutPreset.TopLeft);
titleBar.OffsetLeft = 12;
titleBar.OffsetTop = 8;
titleBar.AddThemeConstantOverride("separation", 12);
var backButton = new Button { Text = "← Menu", CustomMinimumSize = new Vector2(70, 28) };
backButton.AddThemeFontSizeOverride("font_size", 11);
var backStyle = new StyleBoxFlat
{
BgColor = new Color("#2A2A2E"),
BorderColor = new Color("#444448"),
BorderWidthBottom = 1, BorderWidthTop = 1, BorderWidthLeft = 1, BorderWidthRight = 1,
CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4,
CornerRadiusBottomLeft = 4, CornerRadiusBottomRight = 4,
ContentMarginLeft = 8, ContentMarginRight = 8,
ContentMarginTop = 2, ContentMarginBottom = 2
};
backButton.AddThemeStyleboxOverride("normal", backStyle);
backButton.Pressed += OnBackToMenu;
titleBar.AddChild(backButton);
_levelTitle = new Label { Text = "CHESSISTICS" }; _levelTitle = new Label { Text = "CHESSISTICS" };
_levelTitle.SetAnchorsPreset(Control.LayoutPreset.TopLeft);
_levelTitle.OffsetLeft = 16;
_levelTitle.OffsetTop = 12;
_levelTitle.AddThemeFontSizeOverride("font_size", 20); _levelTitle.AddThemeFontSizeOverride("font_size", 20);
_levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore; _levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore;
uiRoot.AddChild(_levelTitle); titleBar.AddChild(_levelTitle);
uiRoot.AddChild(titleBar);
// --- Side Panel (anchored to right edge) --- // --- Side Panel (anchored to right edge) ---
_sidePanel = new PanelContainer(); _sidePanel = new PanelContainer();
@ -204,6 +262,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);
} }
@ -233,12 +300,23 @@ public partial class Main : Node2D
var snap = _sim.GetSnapshot(); var snap = _sim.GetSnapshot();
if (snap.Phase != SimPhase.Edit) return; if (snap.Phase != SimPhase.Edit) return;
_boardView.ClearHighlights();
var coords = new Coords(col, row); var coords = new Coords(col, row);
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords); var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
if (piece != null) if (piece != null)
{
_detailPanel.ShowPiece(piece); _detailPanel.ShowPiece(piece);
// Highlight start and end cells to show trajectory
var pieceColor = PieceView.GetPieceColor(piece.Kind);
var highlightColor = new Color(pieceColor, 0.3f);
_boardView.HighlightCells([piece.StartCell, piece.EndCell], highlightColor);
}
else else
{
_detailPanel.Hide(); _detailPanel.Hide();
}
} }
// --- Level Management --- // --- Level Management ---
@ -254,8 +332,15 @@ public partial class Main : Node2D
private void OnLevelSelected(int levelIndex) private void OnLevelSelected(int levelIndex)
{ {
SfxManager.Instance?.PlayClick();
_currentLevelIndex = levelIndex; _currentLevelIndex = levelIndex;
LoadLevel(levelIndex);
// Fade out, load, fade in
FadeOut(0.25f, () =>
{
LoadLevel(levelIndex);
FadeIn(0.3f);
});
} }
private void LoadLevel(int index) private void LoadLevel(int index)
@ -370,17 +455,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,
@ -453,13 +534,32 @@ public partial class Main : Node2D
_running = false; _running = false;
_simTimer.Stop(); _simTimer.Stop();
_sim.ProcessCommand(new StopSimulationCommand()); _sim.ProcessCommand(new StopSimulationCommand());
_eventAnimator.ResetPiecePositions(_sim.GetSnapshot());
// Full visual rebuild: clear everything and recreate from snapshot
_eventAnimator.ClearAll();
var snap = _sim.GetSnapshot();
foreach (var ps in snap.Pieces)
{
var pieceView = new PieceView();
pieceView.Setup(ps.Id, ps.Kind, ps.StartCell, ps.EndCell, _boardView);
_boardView.AddChild(pieceView);
var color = PieceView.GetPieceColor(ps.Kind);
var trajectView = new TrajectView();
trajectView.Setup(ps.Id,
_boardView.CoordsToPixel(ps.StartCell),
_boardView.CoordsToPixel(ps.EndCell),
color);
_boardView.AddChild(trajectView);
_eventAnimator.RegisterPiece(ps.Id, pieceView, trajectView);
}
_controlBar.UpdateForPhase(SimPhase.Edit); _controlBar.UpdateForPhase(SimPhase.Edit);
_controlBar.ResetTurn(); _controlBar.ResetTurn();
_metricsOverlay.Hide(); _metricsOverlay.Hide();
_inputMapper.SetSnapshot(_sim.GetSnapshot()); _inputMapper.SetSnapshot(snap);
// Reset objective panel
if (_currentLevel != null) if (_currentLevel != null)
_objectivePanel.Setup(_currentLevel.Demands); _objectivePanel.Setup(_currentLevel.Demands);
} }
@ -512,4 +612,18 @@ public partial class Main : Node2D
else else
ShowLevelSelect(); ShowLevelSelect();
} }
private void OnBackToMenu()
{
SfxManager.Instance?.PlayClick();
_running = false;
_simTimer.Stop();
_eventAnimator.ClearAll();
FadeOut(0.2f, () =>
{
ShowLevelSelect();
FadeIn(0.3f);
});
}
} }

View file

@ -6,21 +6,26 @@ 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 QueenColor = new("#8E3D5A"); // deep burgundy
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 +36,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);
@ -63,6 +78,7 @@ public partial class PieceView : Node2D
PieceKind.Rook => "T", PieceKind.Rook => "T",
PieceKind.Bishop => "F", PieceKind.Bishop => "F",
PieceKind.Knight => "C", PieceKind.Knight => "C",
PieceKind.Queen => "D",
_ => "?" _ => "?"
}, },
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
@ -71,25 +87,47 @@ 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,
PieceKind.Queen => QueenColor,
_ => 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 +138,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 +155,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 +166,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) tween.TweenCallback(Callable.From(() =>
{ {
var pieceId = destroyed.PieceId; SfxManager.Instance?.PlayDestroy();
tween.TweenCallback(Callable.From(() => foreach (var destroyed in captured)
{ {
FlashPiece(pieceId); if (_pieceViews.TryGetValue(destroyed.PieceId, out var pv))
UnregisterPiece(pieceId); {
})); SpawnDestroyParticles(pv.Position);
}
tween.SetParallel(false); 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.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);
{
CargoType.Wood => WoodCargoColor,
CargoType.Stone => StoneCargoColor,
_ => Colors.White
};
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), Size = new Vector2(12, 12),
Position = new Vector2(-7, -7), Position = new Vector2(-6, -6),
Color = color, Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore MouseFilter = Control.MouseFilterEnum.Ignore
}; };
container.AddChild(main);
var container = new Node2D { Position = from }; // Trail particles (2 smaller squares that follow with delay)
container.AddChild(sprite); for (int i = 1; i <= 2; i++)
_boardView.AddChild(container); {
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(); var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration) slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.InOut) .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
.SetTrans(Tween.TransitionType.Cubic);
slideTween.TweenCallback(Callable.From(() => container.QueueFree())); 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 center = _boardView.CoordsToPixel(cell);
var tween = pv.CreateTween(); var color = GetCargoColor(type);
tween.TweenProperty(pv, "modulate", new Color(1, 0.2f, 0.2f), 0.1f); var rng = new Random();
tween.TweenProperty(pv, "modulate", Colors.White, 0.1f);
tween.SetLoops(3); 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) 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

@ -0,0 +1 @@
uid://budg72rej2hx3

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

@ -1,5 +1,6 @@
using Godot; using Godot;
using Chessistics.Engine.Model; using Chessistics.Engine.Model;
using Chessistics.Scripts.Pieces;
namespace Chessistics.Scripts.UI; namespace Chessistics.Scripts.UI;
@ -14,17 +15,41 @@ public partial class DetailPanel : PanelContainer
public override void _Ready() public override void _Ready()
{ {
var style = new StyleBoxFlat
{
BgColor = new Color(0.14f, 0.14f, 0.16f, 0.95f),
BorderColor = new Color("#444448"),
BorderWidthTop = 1,
CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4,
ContentMarginLeft = 12, ContentMarginRight = 12,
ContentMarginTop = 8, ContentMarginBottom = 8
};
AddThemeStyleboxOverride("panel", style);
var vbox = new VBoxContainer(); var vbox = new VBoxContainer();
vbox.AddThemeConstantOverride("separation", 4);
var title = new Label { Text = "DETAIL" }; var title = new Label { Text = "DETAIL" };
title.AddThemeFontSizeOverride("font_size", 14); title.AddThemeFontSizeOverride("font_size", 13);
title.AddThemeColorOverride("font_color", new Color("#B8942A"));
vbox.AddChild(title); vbox.AddChild(title);
_infoLabel = new Label { Text = "" }; _infoLabel = new Label { Text = "" };
_infoLabel.AddThemeFontSizeOverride("font_size", 11); _infoLabel.AddThemeFontSizeOverride("font_size", 11);
_infoLabel.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
vbox.AddChild(_infoLabel); vbox.AddChild(_infoLabel);
_removeButton = new Button { Text = "Retirer" }; _removeButton = new Button { Text = "Retirer", CustomMinimumSize = new Vector2(80, 26) };
_removeButton.AddThemeFontSizeOverride("font_size", 11);
var btnStyle = new StyleBoxFlat
{
BgColor = new Color("#5A2A2A"),
CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4,
CornerRadiusBottomLeft = 4, CornerRadiusBottomRight = 4,
ContentMarginLeft = 8, ContentMarginRight = 8,
ContentMarginTop = 2, ContentMarginBottom = 2
};
_removeButton.AddThemeStyleboxOverride("normal", btnStyle);
_removeButton.Pressed += () => EmitSignal(SignalName.RemoveRequested, _currentPieceId); _removeButton.Pressed += () => EmitSignal(SignalName.RemoveRequested, _currentPieceId);
vbox.AddChild(_removeButton); vbox.AddChild(_removeButton);
@ -37,9 +62,11 @@ public partial class DetailPanel : PanelContainer
_currentPieceId = piece.Id; _currentPieceId = piece.Id;
var kindName = piece.Kind switch var kindName = piece.Kind switch
{ {
PieceKind.Pawn => "Pion",
PieceKind.Rook => "Tour II", PieceKind.Rook => "Tour II",
PieceKind.Bishop => "Fou II", PieceKind.Bishop => "Fou II",
PieceKind.Knight => "Cavalier", PieceKind.Knight => "Cavalier",
PieceKind.Queen => "Dame",
_ => piece.Kind.ToString() _ => piece.Kind.ToString()
}; };

View file

@ -15,7 +15,9 @@ public partial class LevelSelectScreen : Control
("Le Col", "Franchissez le mur et gerez deux types de cargaison."), ("Le Col", "Franchissez le mur et gerez deux types de cargaison."),
("Le Carrefour", "Deux productions, deux demandes, et un carrefour au centre."), ("Le Carrefour", "Deux productions, deux demandes, et un carrefour au centre."),
("Le Labyrinthe", "Un couloir etroit serpente a travers les murs."), ("Le Labyrinthe", "Un couloir etroit serpente a travers les murs."),
("Trois Royaumes", "Trois productions, trois demandes. Gerez un reseau complet.") ("Trois Royaumes", "Trois productions, trois demandes. Gerez un reseau complet."),
("La Dame Blanche", "La Dame entre en jeu. Portee supreme sur 8 directions."),
("Le Grand Reseau", "Quatre productions, quatre demandes. Reseau complet.")
]; ];
public override void _Ready() public override void _Ready()

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

View file

@ -2,6 +2,7 @@ using Godot;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Chessistics.Engine.Model; using Chessistics.Engine.Model;
using Chessistics.Scripts.Pieces;
namespace Chessistics.Scripts.UI; namespace Chessistics.Scripts.UI;
@ -15,6 +16,13 @@ public partial class PieceStockPanel : VBoxContainer
public PieceKind? SelectedKind => _selectedKind; public PieceKind? SelectedKind => _selectedKind;
private static readonly Color NormalBg = new("#2A2A2E");
private static readonly Color SelectedBg = new("#3D6B8E");
private static readonly Color HoverBg = new("#353538");
private static readonly Color DisabledBg = new("#1E1E20");
private static readonly Color BorderNormal = new("#444448");
private static readonly Color BorderSelected = new("#5A9ECC");
public void Setup(IReadOnlyList<PieceStock> stock) public void Setup(IReadOnlyList<PieceStock> stock)
{ {
foreach (var child in GetChildren()) foreach (var child in GetChildren())
@ -24,7 +32,7 @@ public partial class PieceStockPanel : VBoxContainer
var title = new Label { Text = "PIECES" }; var title = new Label { Text = "PIECES" };
title.AddThemeFontSizeOverride("font_size", 16); title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#FFD700")); title.AddThemeColorOverride("font_color", new Color("#B8942A"));
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());
@ -32,16 +40,31 @@ public partial class PieceStockPanel : VBoxContainer
foreach (var entry in stock) foreach (var entry in stock)
{ {
var hbox = new HBoxContainer(); var hbox = new HBoxContainer();
hbox.AddThemeConstantOverride("separation", 8);
// Color dot matching piece color
var dot = new ColorRect
{
CustomMinimumSize = new Vector2(10, 10),
Size = new Vector2(10, 10),
Color = PieceView.GetPieceColor(entry.Kind),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var dotCenter = new CenterContainer { CustomMinimumSize = new Vector2(14, 32) };
dotCenter.AddChild(dot);
hbox.AddChild(dotCenter);
var button = new Button var button = new Button
{ {
Text = GetPieceName(entry.Kind), Text = GetPieceName(entry.Kind),
CustomMinimumSize = new Vector2(120, 32), CustomMinimumSize = new Vector2(120, 32),
ToggleMode = true ToggleMode = false // We manage selection state ourselves
}; };
ApplyButtonStyle(button, false);
var countLabel = new Label { Text = $"x{entry.Count}" }; var countLabel = new Label { Text = $"x{entry.Count}" };
countLabel.AddThemeFontSizeOverride("font_size", 14); countLabel.AddThemeFontSizeOverride("font_size", 13);
countLabel.AddThemeColorOverride("font_color", new Color("#999999"));
var kind = entry.Kind; var kind = entry.Kind;
button.Pressed += () => OnPieceButtonPressed(kind); button.Pressed += () => OnPieceButtonPressed(kind);
@ -72,11 +95,45 @@ public partial class PieceStockPanel : VBoxContainer
{ {
foreach (var (k, (button, _, remaining)) in _entries) foreach (var (k, (button, _, remaining)) in _entries)
{ {
button.ButtonPressed = k == _selectedKind; bool selected = k == _selectedKind;
button.Disabled = remaining <= 0; button.Disabled = remaining <= 0;
ApplyButtonStyle(button, selected);
} }
} }
private static void ApplyButtonStyle(Button button, bool selected)
{
var bg = selected ? SelectedBg : NormalBg;
var border = selected ? BorderSelected : BorderNormal;
var normal = MakeStyle(bg, border);
var hover = MakeStyle(selected ? SelectedBg.Lightened(0.1f) : HoverBg, border);
var disabled = MakeStyle(DisabledBg, new Color("#2A2A2E"));
button.AddThemeStyleboxOverride("normal", normal);
button.AddThemeStyleboxOverride("hover", hover);
button.AddThemeStyleboxOverride("pressed", MakeStyle(bg.Darkened(0.15f), border));
button.AddThemeStyleboxOverride("disabled", disabled);
button.AddThemeFontSizeOverride("font_size", 12);
button.AddThemeColorOverride("font_color", selected ? Colors.White : new Color("#CCCCCC"));
button.AddThemeColorOverride("font_disabled_color", new Color("#555555"));
}
private static StyleBoxFlat MakeStyle(Color bg, Color border)
{
return new StyleBoxFlat
{
BgColor = bg,
BorderColor = border,
BorderWidthBottom = 1, BorderWidthTop = 1,
BorderWidthLeft = 1, BorderWidthRight = 1,
CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4,
CornerRadiusBottomLeft = 4, CornerRadiusBottomRight = 4,
ContentMarginLeft = 8, ContentMarginRight = 8,
ContentMarginTop = 4, ContentMarginBottom = 4
};
}
public void UpdateCount(PieceKind kind, int remaining) public void UpdateCount(PieceKind kind, int remaining)
{ {
if (!_entries.TryGetValue(kind, out var entry)) return; if (!_entries.TryGetValue(kind, out var entry)) return;
@ -97,6 +154,7 @@ public partial class PieceStockPanel : VBoxContainer
PieceKind.Rook => "Tour II", PieceKind.Rook => "Tour II",
PieceKind.Bishop => "Fou II", PieceKind.Bishop => "Fou II",
PieceKind.Knight => "Cavalier", PieceKind.Knight => "Cavalier",
PieceKind.Queen => "Dame",
_ => kind.ToString() _ => kind.ToString()
}; };
} }

View file

@ -56,6 +56,7 @@ public static class LevelLoader
"rook" => PieceKind.Rook, "rook" => PieceKind.Rook,
"bishop" => PieceKind.Bishop, "bishop" => PieceKind.Bishop,
"knight" => PieceKind.Knight, "knight" => PieceKind.Knight,
"queen" => PieceKind.Queen,
_ => throw new JsonException($"Unknown piece kind: '{kind}'") _ => throw new JsonException($"Unknown piece kind: '{kind}'")
}; };

View file

@ -5,5 +5,6 @@ public enum PieceKind
Pawn, Pawn,
Rook, Rook,
Bishop, Bishop,
Knight Knight,
Queen
} }

View file

@ -8,6 +8,7 @@ public static class PieceRules
PieceKind.Rook => 5, PieceKind.Rook => 5,
PieceKind.Bishop => 3, PieceKind.Bishop => 3,
PieceKind.Knight => 3, PieceKind.Knight => 3,
PieceKind.Queen => 7,
_ => throw new ArgumentOutOfRangeException(nameof(kind)) _ => throw new ArgumentOutOfRangeException(nameof(kind))
}; };
@ -17,6 +18,7 @@ public static class PieceRules
PieceKind.Rook => 2, PieceKind.Rook => 2,
PieceKind.Bishop => 2, PieceKind.Bishop => 2,
PieceKind.Knight => 0, // Knight uses L-shape, not range PieceKind.Knight => 0, // Knight uses L-shape, not range
PieceKind.Queen => 2,
_ => throw new ArgumentOutOfRangeException(nameof(kind)) _ => throw new ArgumentOutOfRangeException(nameof(kind))
}; };
} }

View file

@ -6,6 +6,7 @@ public static class MoveValidator
{ {
private static readonly (int dc, int dr)[] OrthogonalDirs = [(0, 1), (0, -1), (1, 0), (-1, 0)]; private static readonly (int dc, int dr)[] OrthogonalDirs = [(0, 1), (0, -1), (1, 0), (-1, 0)];
private static readonly (int dc, int dr)[] DiagonalDirs = [(1, 1), (1, -1), (-1, 1), (-1, -1)]; private static readonly (int dc, int dr)[] DiagonalDirs = [(1, 1), (1, -1), (-1, 1), (-1, -1)];
private static readonly (int dc, int dr)[] AllDirs = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)];
private static readonly (int dc, int dr)[] KnightOffsets = private static readonly (int dc, int dr)[] KnightOffsets =
[ [
(1, 2), (2, 1), (2, -1), (1, -2), (1, 2), (2, 1), (2, -1), (1, -2),
@ -23,6 +24,7 @@ public static class MoveValidator
PieceKind.Rook => GetSlidingMoves(start, OrthogonalDirs, 2, board), PieceKind.Rook => GetSlidingMoves(start, OrthogonalDirs, 2, board),
PieceKind.Bishop => GetSlidingMoves(start, DiagonalDirs, 2, board), PieceKind.Bishop => GetSlidingMoves(start, DiagonalDirs, 2, board),
PieceKind.Knight => GetKnightMoves(start, board), PieceKind.Knight => GetKnightMoves(start, board),
PieceKind.Queen => GetSlidingMoves(start, AllDirs, 2, board),
_ => [] _ => []
}; };
} }

View file

@ -17,7 +17,7 @@ Chaque piece est un **maillon de convoyeur**. La strategie est dans la compositi
``` ```
OBSERVER la situation (productions, demandes, terrain, pieces disponibles) OBSERVER la situation (productions, demandes, terrain, pieces disponibles)
| |
PLACER des pieces sur le plateau (point de depart + point d'arrivee) PLACER des pieces sur le plateau (point de depart + point d'arrivee)
| |
LANCER la simulation — les pieces font leurs allers-retours, LANCER la simulation — les pieces font leurs allers-retours,
@ -145,6 +145,24 @@ C'est tout. Pas de programmation, pas de route multi-etapes. La piece fait l'all
- **Saute par-dessus** les murs et les autres pieces - **Saute par-dessus** les murs et les autres pieces
- Statut social : **3** - Statut social : **3**
#### Dame
```
X X X
X X X
X X X
X X X [Dame] X X X
X X X
X X X
X X X
```
- Se deplace de **1 ou 2 cases** dans les **8 directions** (horizontal, vertical et diagonal)
- Combine les mouvements de la Tour et du Fou
- Ne peut pas traverser les murs ni les autres pieces
- Statut social : **7** (le plus eleve — prioritaire sur toutes les autres pieces)
- Piece la plus puissante mais rare — force des choix strategiques
> A statut egal, la piece de **niveau le plus eleve** est consideree superieure (ex: Tour II > Tour I, Fou III > Cavalier II). En cas d'egalite parfaite, le departage se fait par **direction en sens horaire** depuis la piece qui donne (voir 4.3). > A statut egal, la piece de **niveau le plus eleve** est consideree superieure (ex: Tour II > Tour I, Fou III > Cavalier II). En cas d'egalite parfaite, le departage se fait par **direction en sens horaire** depuis la piece qui donne (voir 4.3).
### 3.3 Occupation et collision ### 3.3 Occupation et collision
@ -188,6 +206,7 @@ Quand plusieurs transferts sont possibles au meme point, la priorite determine l
``` ```
Hierarchie de statut social (proto) : Hierarchie de statut social (proto) :
Dame 7
Tour 5 Tour 5
Fou 3 Fou 3
Cavalier 3 Cavalier 3
@ -203,7 +222,7 @@ Cette alternance empeche un biais permanent vers une direction et cree des patte
**Exemple** : **Exemple** :
``` ```
Tour (colis) ─adjacent─ case vide ─adjacent─ Cavalier (vide) Tour (colis) ─adjacent─ case vide ─adjacent─ Cavalier (vide)
─adjacent─ Tour (vide) ─adjacent─ Tour (vide)
``` ```
La Tour avec colis donne. Deux receveurs possibles : Cavalier (3) et Tour (5). La Tour recoit (statut 5 > 3). La Tour avec colis donne. Deux receveurs possibles : Cavalier (3) et Tour (5). La Tour recoit (statut 5 > 3).
@ -546,8 +565,8 @@ Le Fou peut couvrir des trajets diagonaux que les Tours ne peuvent pas. Pour att
3 . . # . . . 3 . . # . . .
2 . . . . . . 2 . . . . . .
1 [S1] . . . . [S2] Scierie (Bois) 1 [S1] . . . . [S2] Scierie (Bois)
Carriere (Pierre) Carriere (Pierre)
a b c d e f a b c d e f
``` ```
- Plateau : **6x6** - Plateau : **6x6**
@ -628,7 +647,7 @@ Le Cavalier saute le mur en L. Il peut connecter les deux cotes du plateau la ou
2 . . . . # . # . 2 . . . . # . # .
1 [S1] . . . # . . [D2] Forge — 3 Pierre en 50 coups 1 [S1] . . . # . . [D2] Forge — 3 Pierre en 50 coups
a b c d e f g h a b c d e f g h
``` ```
- Plateau : **8x6** - Plateau : **8x6**
@ -717,29 +736,29 @@ Le prototype vise la lisibilite.
``` ```
Chessistics/ Chessistics/
scenes/ scenes/
Main.tscn Main.tscn
Board/ Board/
Board.tscn — Le damier Board.tscn — Le damier
Cell.tscn — Une case Cell.tscn — Une case
Pieces/ Pieces/
Piece.tscn — Scene piece (silhouette + cube cargaison) Piece.tscn — Scene piece (silhouette + cube cargaison)
TrajectView.tscn — Trait visuel du trajet (Line2D) TrajectView.tscn — Trait visuel du trajet (Line2D)
UI/ UI/
ObjectivePanel.tscn — Objectifs + stock de pieces ObjectivePanel.tscn — Objectifs + stock de pieces
DetailPanel.tscn — Detail piece selectionnee DetailPanel.tscn — Detail piece selectionnee
ControlBar.tscn — Play / pause / stop / vitesse ControlBar.tscn — Play / pause / stop / vitesse
MetricsOverlay.tscn — Resultats post-victoire MetricsOverlay.tscn — Resultats post-victoire
LevelSelect.tscn — Selection de niveau LevelSelect.tscn — Selection de niveau
scripts/ scripts/
Core/ Core/
Board.cs — Grille, cases, adjacence Board.cs — Grille, cases, adjacence
Cell.cs — Type de case, contenu Cell.cs — Type de case, contenu
Piece.cs — Type, statut, mouvement, cargaison Piece.cs — Type, statut, mouvement, cargaison
PieceType.cs — Enum + regles de mouvement + statut social PieceType.cs — Enum + regles de mouvement + statut social
TransferResolver.cs — Logique de transfert (adjacence, priorite, statut) TransferResolver.cs — Logique de transfert (adjacence, priorite, statut)
Executor.cs — Moteur de simulation (coups, collisions, transferts) Executor.cs — Moteur de simulation (coups, collisions, transferts)
Data/ Data/
Level.cs — Definition d'un niveau Level.cs — Definition d'un niveau
LevelLoader.cs — Chargement JSON LevelLoader.cs — Chargement JSON
UI/ UI/
PiecePlacer.cs — Logique du placement 2 clics PiecePlacer.cs — Logique du placement 2 clics
@ -762,14 +781,14 @@ Chessistics/
"width": 4, "width": 4,
"height": 4, "height": 4,
"productions": [ "productions": [
{ "x": 0, "y": 0, "name": "Scierie", "cargo": "wood", "interval": 2 } { "x": 0, "y": 0, "name": "Scierie", "cargo": "wood", "interval": 2 }
], ],
"demands": [ "demands": [
{ "x": 3, "y": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 } { "x": 3, "y": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 }
], ],
"walls": [], "walls": [],
"pieces": [ "pieces": [
{ "type": "rook", "level": 2, "count": 3 } { "type": "rook", "level": 2, "count": 3 }
] ]
} }
``` ```