Compare commits

..

No commits in common. "358ab48d596406b8bc540717e0cb6b15a327f558" and "e1218b3eaa43deb9228c37eb863d04164cd9b37d" have entirely different histories.

22 changed files with 233 additions and 1085 deletions

View file

@ -24,9 +24,3 @@ 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.

View file

@ -1,28 +0,0 @@
{
"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 }
]
}

View file

@ -1,33 +0,0 @@
{
"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,28 +28,22 @@
- 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: Dame, network levels, juice pass (DONE) ## Phase 5: Network levels and Dame (Queen)
- Dame (Queen): 8 directions, range 2, status 7 (highest) **Goal**: Open-ended logistics puzzles with interconnected supply networks.
- 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
## Phase 6: Godot integration (DONE) - Multiple productions feeding multiple demands through shared infrastructure.
- 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).
- Board renderer, piece placement, step/play/pause controls ## Phase 6: Godot integration
- Event visualization with simultaneous animations per phase
- Victory/defeat screens with animated metrics
- Production flash, cargo slide trails, destruction particles, confetti
## Phase 7: Zoom, scroll wheel, and camera polish **Goal**: Playable visual prototype.
- Mouse scroll wheel to zoom in/out on board - Board renderer: grid, walls, buildings, pieces.
- Zoom limits (min/max) to prevent getting lost - Drag-and-drop piece placement during Edit phase.
- Double-click to center on a piece - Step/play/pause simulation controls.
- Event visualization: cargo movement, transfers, delivery animations.
## Phase 8: Level editor (future) - Victory/defeat screens with Metrics display.
- Player-designed levels via JSON export
- In-game editing of board size, walls, productions, demands, stock

View file

@ -7,7 +7,6 @@ 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)
@ -18,14 +17,13 @@ public partial class CellView : Node2D
public Coords Coords { get; private set; } public Coords Coords { get; private set; }
// Warmer, more grounded palette private static readonly Color LightColor = new("#F0D9B5");
private static readonly Color LightColor = new("#E8D5A8"); // warm parchment private static readonly Color DarkColor = new("#B58863");
private static readonly Color DarkColor = new("#A07850"); // warm walnut private static readonly Color WallColor = new("#555555");
private static readonly Color WallColor = new("#3A3A3A"); // charcoal private static readonly Color ProductionColor = new("#6B8E5A");
private static readonly Color ProductionColor = new("#4A6E3A"); // deep forest private static readonly Color DemandColor = new("#C9A833");
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("#FFFFFF88"); private static readonly Color HoverOutlineColor = new("#FFFFFFAA");
private const int OutlineWidth = 2; private const int OutlineWidth = 2;
@ -51,17 +49,6 @@ 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),
@ -73,34 +60,54 @@ public partial class CellView : Node2D
AddChild(_highlight); AddChild(_highlight);
// Hover outline (4 border rects) // Hover outline (4 border rects)
_hoverTop = CreateBorderRect(new Vector2(cellSize, OutlineWidth), Vector2.Zero); _hoverTop = new ColorRect
_hoverBottom = CreateBorderRect(new Vector2(cellSize, OutlineWidth), new Vector2(0, cellSize - OutlineWidth));
_hoverLeft = CreateBorderRect(new Vector2(OutlineWidth, cellSize), Vector2.Zero);
_hoverRight = CreateBorderRect(new Vector2(OutlineWidth, cellSize), new Vector2(cellSize - OutlineWidth, 0));
_label = new Label
{ {
Position = new Vector2(4, 3), Size = new Vector2(cellSize, OutlineWidth),
Text = "", Position = Vector2.Zero,
MouseFilter = Control.MouseFilterEnum.Ignore
};
_label.AddThemeFontSizeOverride("font_size", 9);
_label.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.7f));
AddChild(_label);
}
private ColorRect CreateBorderRect(Vector2 size, Vector2 pos)
{
var rect = new ColorRect
{
Size = size,
Position = pos,
Color = HoverOutlineColor, Color = HoverOutlineColor,
Visible = false, Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore MouseFilter = Control.MouseFilterEnum.Ignore
}; };
AddChild(rect); AddChild(_hoverTop);
return rect;
_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
{
Position = new Vector2(2, 2),
Text = "",
MouseFilter = Control.MouseFilterEnum.Ignore
};
_label.AddThemeFontSizeOverride("font_size", 10);
AddChild(_label);
} }
public void SetLabel(string text) => _label.Text = text; public void SetLabel(string text) => _label.Text = text;
@ -121,15 +128,14 @@ public partial class CellView : Node2D
} }
/// <summary> /// <summary>
/// Production pulse: warm glow that radiates outward. /// Brief white flash on the cell to signal production.
/// </summary> /// </summary>
public void FlashProduce(float duration = 0.3f) public void FlashProduce(float duration = 0.3f)
{ {
_highlight.Color = new Color(0.9f, 0.85f, 0.5f, 0.55f); // warm golden _highlight.Color = new Color(1, 1, 1, 0.5f);
_highlight.Visible = true; _highlight.Visible = true;
var tween = CreateTween(); var tween = CreateTween();
tween.TweenProperty(_highlight, "color", new Color(0.9f, 0.85f, 0.5f, 0f), duration) tween.TweenProperty(_highlight, "color", new Color(1, 1, 1, 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,5 +1,4 @@
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;
@ -38,7 +37,6 @@ 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!;
@ -46,7 +44,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", "level_07.json", "level_08.json"]; private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json", "level_04.json", "level_05.json", "level_06.json"];
private const float SidePanelWidth = 280f; private const float SidePanelWidth = 280f;
private const float ControlBarHeight = 48f; private const float ControlBarHeight = 48f;
@ -60,9 +58,6 @@ 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)
@ -71,10 +66,6 @@ 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)
{ {
@ -82,40 +73,12 @@ 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);
@ -144,35 +107,14 @@ public partial class Main : Node2D
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore; uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore;
_uiLayer.AddChild(uiRoot); _uiLayer.AddChild(uiRoot);
// Level title bar (top-left) // Level title (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;
titleBar.AddChild(_levelTitle); uiRoot.AddChild(_levelTitle);
uiRoot.AddChild(titleBar);
// --- Side Panel (anchored to right edge) --- // --- Side Panel (anchored to right edge) ---
_sidePanel = new PanelContainer(); _sidePanel = new PanelContainer();
@ -262,15 +204,6 @@ 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);
} }
@ -300,23 +233,12 @@ 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 ---
@ -332,15 +254,8 @@ 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)
@ -455,13 +370,17 @@ 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 = PieceView.GetPieceColor(placed.Kind); var color = placed.Kind switch
{
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,
@ -534,32 +453,13 @@ 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(snap); _inputMapper.SetSnapshot(_sim.GetSnapshot());
// Reset objective panel
if (_currentLevel != null) if (_currentLevel != null)
_objectivePanel.Setup(_currentLevel.Demands); _objectivePanel.Setup(_currentLevel.Demands);
} }
@ -612,18 +512,4 @@ 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,26 +6,21 @@ 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; }
// Muted, earthy palette — teal, sienna, gold tones private static readonly Color PawnColor = new("#7AB54A");
private static readonly Color PawnColor = new("#5A8C6B"); // sage green private static readonly Color RookColor = new("#4A7AB5");
private static readonly Color RookColor = new("#3D6B8E"); // deep teal private static readonly Color BishopColor = new("#B54A8E");
private static readonly Color BishopColor = new("#8E5A6B"); // dusty rose private static readonly Color KnightColor = new("#B5824A");
private static readonly Color KnightColor = new("#8E7A3D"); // burnt sienna private static readonly Color WoodCargoColor = new("#8B6914");
private static readonly Color QueenColor = new("#8E3D5A"); // deep burgundy 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)
{ {
@ -36,36 +31,26 @@ public partial class PieceView : Node2D
Position = boardView.CoordsToPixel(startCell); Position = boardView.CoordsToPixel(startCell);
var color = GetPieceColor(kind); var color = kind switch
// Shadow (slightly offset, rendered first)
_shadow = new Sprite2D();
var shadowTex = new GradientTexture2D
{ {
Width = 44, Height = 44, PieceKind.Pawn => PawnColor,
Fill = GradientTexture2D.FillEnum.Radial, PieceKind.Rook => RookColor,
Gradient = new Gradient() PieceKind.Bishop => BishopColor,
PieceKind.Knight => KnightColor,
_ => Colors.White
}; };
shadowTex.Gradient.SetColor(0, ShadowColor);
shadowTex.Gradient.SetColor(1, new Color(0, 0, 0, 0));
_shadow.Texture = shadowTex;
_shadow.Position = new Vector2(3, 5); // subtle offset down-right
AddChild(_shadow);
// Piece body (circle with inner glow) // Piece body (circle)
_sprite = new Sprite2D(); _sprite = new Sprite2D();
var texture = new GradientTexture2D var texture = new GradientTexture2D
{ {
Width = 48, Height = 48, Width = 48,
Height = 48,
Fill = GradientTexture2D.FillEnum.Radial, Fill = GradientTexture2D.FillEnum.Radial,
Gradient = new Gradient() Gradient = new Gradient()
}; };
texture.Gradient.Colors = [ texture.Gradient.SetColor(0, color);
color.Lightened(0.15f), // bright center texture.Gradient.SetColor(1, color.Darkened(0.3f));
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);
@ -78,7 +63,6 @@ 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,
@ -87,47 +71,25 @@ 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", new Color(1, 1, 1, 0.9f)); _label.AddThemeColorOverride("font_color", Colors.White);
AddChild(_label); AddChild(_label);
// Cargo indicator (hidden by default) // Cargo indicator (hidden by default)
_cargoIndicator = new ColorRect _cargoIndicator = new ColorRect
{ {
Size = new Vector2(12, 12), Size = new Vector2(14, 14),
Position = new Vector2(-6, -28), Position = new Vector2(-7, -30),
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;
} }
@ -138,16 +100,6 @@ 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)
@ -155,6 +107,7 @@ 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 =>
{ {
@ -166,8 +119,7 @@ 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,34 +5,15 @@ 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 = 2.5f; Width = 3f;
DefaultColor = new Color(color, 0.35f); DefaultColor = new Color(color, 0.5f);
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("#A67C32"); private static readonly Color WoodCargoColor = new("#8B6914");
private static readonly Color StoneCargoColor = new("#7A7A7A"); private static readonly Color StoneCargoColor = new("#808080");
private const float ProduceDuration = 0.35f; private const float ProduceDuration = 0.3f;
private const float TransferDuration = 0.28f; private const float TransferDuration = 0.25f;
private const float MoveDuration = 0.32f; private const float MoveDuration = 0.3f;
private const float KnightMoveDuration = 0.42f; private const float KnightMoveDuration = 0.4f;
private const float DestroyDuration = 0.45f; private const float DestroyDuration = 0.3f;
[Signal] [Signal]
public delegate void TurnAnimationCompletedEventHandler(); public delegate void TurnAnimationCompletedEventHandler();
@ -107,8 +107,6 @@ 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);
})); }));
@ -139,50 +137,45 @@ public partial class EventAnimator : Node
List<PieceMovedEvent> moveEvents, List<PieceMovedEvent> moveEvents,
List<PieceDestroyedEvent> collisionEvents) List<PieceDestroyedEvent> collisionEvents)
{ {
// Phase 1: Produce — warm golden flash + particle burst // Phase 1: Produce — flash production cells
if (produceEvents.Count > 0) if (produceEvents.Count > 0)
{ {
var captured = produceEvents.ToList();
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
SfxManager.Instance?.PlayProduce(); foreach (var evt in produceEvents.ToList())
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 — cargo slides with trail particles // Phase 2: Transfers — animate cargo sliding from giver to receiver
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(() =>
{ {
@ -203,10 +196,9 @@ public partial class EventAnimator : Node
transferEvents.Clear(); transferEvents.Clear();
} }
// Phase 3: Movement — simultaneous, with sfx // Phase 3: Movement — all pieces move simultaneously
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)
{ {
@ -214,218 +206,74 @@ 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 — shrink + spin + particles // Phase 4: Collision/Destruction
if (collisionEvents.Count > 0) if (collisionEvents.Count > 0)
{ {
var captured = collisionEvents.ToList(); tween.SetParallel(true);
tween.TweenCallback(Callable.From(() => foreach (var destroyed in collisionEvents)
{ {
SfxManager.Instance?.PlayDestroy(); var pieceId = destroyed.PieceId;
foreach (var destroyed in captured) tween.TweenCallback(Callable.From(() =>
{ {
if (_pieceViews.TryGetValue(destroyed.PieceId, out var pv)) FlashPiece(pieceId);
{ UnregisterPiece(pieceId);
SpawnDestroyParticles(pv.Position); }));
}
var dt = pv.CreateTween(); tween.SetParallel(false);
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();
} }
} }
// --- Visual Effects --- /// <summary>
/// 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 = GetCargoColor(transfer.Type); var color = transfer.Type switch
var container = new Node2D { Position = from };
_boardView.AddChild(container);
// Main cargo square
var main = new ColorRect
{ {
Size = new Vector2(12, 12), CargoType.Wood => WoodCargoColor,
Position = new Vector2(-6, -6), CargoType.Stone => StoneCargoColor,
_ => Colors.White
};
var sprite = new ColorRect
{
Size = new Vector2(14, 14),
Position = new Vector2(-7, -7),
Color = color, Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore MouseFilter = Control.MouseFilterEnum.Ignore
}; };
container.AddChild(main);
// Trail particles (2 smaller squares that follow with delay) var container = new Node2D { Position = from };
for (int i = 1; i <= 2; i++) container.AddChild(sprite);
{ _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.Out).SetTrans(Tween.TransitionType.Back); .SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Cubic);
slideTween.TweenCallback(Callable.From(() => container.QueueFree())); slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
} }
private void SpawnProduceParticles(Coords cell, CargoType type) private void FlashPiece(int pieceId)
{ {
var center = _boardView.CoordsToPixel(cell); if (!_pieceViews.TryGetValue(pieceId, out var pv)) return;
var color = GetCargoColor(type); var tween = pv.CreateTween();
var rng = new Random(); tween.TweenProperty(pv, "modulate", new Color(1, 0.2f, 0.2f), 0.1f);
tween.TweenProperty(pv, "modulate", Colors.White, 0.1f);
for (int i = 0; i < 6; i++) tween.SetLoops(3);
{
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

@ -1,160 +0,0 @@
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

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

View file

@ -24,92 +24,38 @@ 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()
{ {
AddThemeConstantOverride("separation", 8); _playButton = new Button { Text = "▶ PLAY" };
_playButton = CreateStyledButton("PLAY");
_playButton.Pressed += () => EmitSignal(SignalName.PlayPressed); _playButton.Pressed += () => EmitSignal(SignalName.PlayPressed);
AddChild(_playButton); AddChild(_playButton);
_pauseButton = CreateStyledButton("PAUSE"); _pauseButton = new Button { Text = "⏸ PAUSE" };
_pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed); _pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed);
AddChild(_pauseButton); AddChild(_pauseButton);
_stepButton = CreateStyledButton("STEP"); _stepButton = new Button { Text = "⏭ STEP" };
_stepButton.Pressed += () => EmitSignal(SignalName.StepPressed); _stepButton.Pressed += () => EmitSignal(SignalName.StepPressed);
AddChild(_stepButton); AddChild(_stepButton);
_stopButton = CreateStyledButton("STOP"); _stopButton = new Button { Text = "⏹ STOP" };
_stopButton.Pressed += () => EmitSignal(SignalName.StopPressed); _stopButton.Pressed += () => EmitSignal(SignalName.StopPressed);
AddChild(_stopButton); AddChild(_stopButton);
// Spacer _speedSelect = new OptionButton();
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", 13); _turnLabel.AddThemeFontSizeOverride("font_size", 14);
_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,6 +1,5 @@
using Godot; using Godot;
using Chessistics.Engine.Model; using Chessistics.Engine.Model;
using Chessistics.Scripts.Pieces;
namespace Chessistics.Scripts.UI; namespace Chessistics.Scripts.UI;
@ -15,41 +14,17 @@ 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", 13); title.AddThemeFontSizeOverride("font_size", 14);
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", CustomMinimumSize = new Vector2(80, 26) }; _removeButton = new Button { Text = "Retirer" };
_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);
@ -62,11 +37,9 @@ 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,9 +15,7 @@ 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,62 +10,46 @@ public partial class MetricsOverlay : PanelContainer
[Signal] [Signal]
public delegate void RetryPressedEventHandler(); public delegate void RetryPressedEventHandler();
private Label _titleLabel = null!; private Label _metricsLabel = 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 style = new StyleBoxFlat
{
BgColor = new Color(0.1f, 0.1f, 0.12f, 0.95f),
BorderColor = new Color("#B8942A"),
BorderWidthBottom = 2, BorderWidthTop = 2,
BorderWidthLeft = 2, BorderWidthRight = 2,
CornerRadiusTopLeft = 12, CornerRadiusTopRight = 12,
CornerRadiusBottomLeft = 12, CornerRadiusBottomRight = 12,
ContentMarginLeft = 32, ContentMarginRight = 32,
ContentMarginTop = 28, ContentMarginBottom = 28
};
AddThemeStyleboxOverride("panel", style);
var vbox = new VBoxContainer(); var vbox = new VBoxContainer();
vbox.AddThemeConstantOverride("separation", 8); vbox.SetAnchorsPreset(LayoutPreset.Center);
_titleLabel = new Label var title = new Label
{ {
Text = "VICTOIRE !", Text = "VICTOIRE !",
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
_titleLabel.AddThemeFontSizeOverride("font_size", 26); title.AddThemeFontSizeOverride("font_size", 24);
_titleLabel.AddThemeColorOverride("font_color", new Color("#FFD700")); title.AddThemeColorOverride("font_color", new Color("#FFD700"));
vbox.AddChild(_titleLabel); vbox.AddChild(title);
vbox.AddChild(new HSeparator()); vbox.AddChild(new HSeparator());
_piecesLabel = CreateMetricLabel(); _metricsLabel = new Label
vbox.AddChild(_piecesLabel); {
_turnsLabel = CreateMetricLabel(); Text = "",
vbox.AddChild(_turnsLabel); HorizontalAlignment = HorizontalAlignment.Center
_cellsLabel = CreateMetricLabel(); };
vbox.AddChild(_cellsLabel); _metricsLabel.AddThemeFontSizeOverride("font_size", 14);
vbox.AddChild(_metricsLabel);
vbox.AddChild(new HSeparator()); vbox.AddChild(new HSeparator());
_buttons = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.Center }; var buttons = new HBoxContainer();
_buttons.AddThemeConstantOverride("separation", 16); buttons.Alignment = BoxContainer.AlignmentMode.Center;
var retryBtn = CreateStyledButton("Rejouer"); var retryBtn = new Button { Text = "Rejouer" };
retryBtn.Pressed += () => EmitSignal(SignalName.RetryPressed); retryBtn.Pressed += () => EmitSignal(SignalName.RetryPressed);
_buttons.AddChild(retryBtn); buttons.AddChild(retryBtn);
var nextBtn = CreateStyledButton("Niveau suivant"); var nextBtn = new Button { Text = "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;
@ -73,87 +57,11 @@ public partial class MetricsOverlay : PanelContainer
public void ShowMetrics(Metrics metrics) public void ShowMetrics(Metrics metrics)
{ {
_piecesLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}"; _metricsLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}\n" +
_turnsLabel.Text = $"Coups: {metrics.TurnsTaken}"; $"Coups: {metrics.TurnsTaken}\n" +
_cellsLabel.Text = $"Cases occupees: {metrics.CellsOccupied}"; $"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() public new void Hide() => Visible = false;
{
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, Label deadline)> _entries = new(); private readonly Dictionary<Coords, (Label label, ProgressBar bar)> _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("#B8942A")); // aged gold title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());
@ -24,11 +24,9 @@ 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
@ -36,34 +34,18 @@ public partial class ObjectivePanel : VBoxContainer
MinValue = 0, MinValue = 0,
MaxValue = demand.Amount, MaxValue = demand.Amount,
Value = 0, Value = 0,
CustomMinimumSize = new Vector2(180, 14), CustomMinimumSize = new Vector2(180, 16),
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("#777777")); deadline.AddThemeColorOverride("font_color", new Color("#AAAAAA"));
vbox.AddChild(deadline); vbox.AddChild(deadline);
AddChild(vbox); AddChild(vbox);
_entries[demand.Position] = (label, bar, deadline); _entries[demand.Position] = (label, bar);
} }
} }
@ -72,24 +54,9 @@ 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,7 +2,6 @@ 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;
@ -16,13 +15,6 @@ 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())
@ -32,7 +24,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("#B8942A")); title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());
@ -40,31 +32,16 @@ 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 = false // We manage selection state ourselves ToggleMode = true
}; };
ApplyButtonStyle(button, false);
var countLabel = new Label { Text = $"x{entry.Count}" }; var countLabel = new Label { Text = $"x{entry.Count}" };
countLabel.AddThemeFontSizeOverride("font_size", 13); countLabel.AddThemeFontSizeOverride("font_size", 14);
countLabel.AddThemeColorOverride("font_color", new Color("#999999"));
var kind = entry.Kind; var kind = entry.Kind;
button.Pressed += () => OnPieceButtonPressed(kind); button.Pressed += () => OnPieceButtonPressed(kind);
@ -95,45 +72,11 @@ public partial class PieceStockPanel : VBoxContainer
{ {
foreach (var (k, (button, _, remaining)) in _entries) foreach (var (k, (button, _, remaining)) in _entries)
{ {
bool selected = k == _selectedKind; button.ButtonPressed = 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;
@ -154,7 +97,6 @@ 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,7 +56,6 @@ 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,6 +5,5 @@ public enum PieceKind
Pawn, Pawn,
Rook, Rook,
Bishop, Bishop,
Knight, Knight
Queen
} }

View file

@ -8,7 +8,6 @@ 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))
}; };
@ -18,7 +17,6 @@ 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,7 +6,6 @@ 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),
@ -24,7 +23,6 @@ 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,24 +145,6 @@ 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
@ -206,7 +188,6 @@ 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
@ -222,7 +203,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).
@ -565,8 +546,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**
@ -647,7 +628,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**
@ -736,29 +717,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
@ -781,14 +762,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 }
] ]
} }
``` ```