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.
**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
- 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)
- 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
**Goal**: Open-ended logistics puzzles with interconnected supply networks.
## 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
- Event visualization with simultaneous animations per phase
- Victory/defeat screens with animated metrics
- Production flash, cargo slide trails, destruction particles, confetti
## Phase 6: Godot integration
## Phase 7: Zoom, scroll wheel, and camera polish
**Goal**: Playable visual prototype.
- Mouse scroll wheel to zoom in/out on board
- Zoom limits (min/max) to prevent getting lost
- Double-click to center on a piece
## Phase 8: Level editor (future)
- Player-designed levels via JSON export
- In-game editing of board size, walls, productions, demands, stock
- Board renderer: grid, walls, buildings, pieces.
- Drag-and-drop piece placement during Edit phase.
- Step/play/pause simulation controls.
- Event visualization: cargo movement, transfers, delivery animations.
- Victory/defeat screens with Metrics display.

View file

@ -7,7 +7,6 @@ public partial class CellView : Node2D
{
private ColorRect _background = null!;
private ColorRect _highlight = null!;
private ColorRect _innerShadow = null!;
private Label _label = null!;
// Hover outline (4 thin rects forming a border)
@ -18,14 +17,13 @@ public partial class CellView : Node2D
public Coords Coords { get; private set; }
// Warmer, more grounded palette
private static readonly Color LightColor = new("#E8D5A8"); // warm parchment
private static readonly Color DarkColor = new("#A07850"); // warm walnut
private static readonly Color WallColor = new("#3A3A3A"); // charcoal
private static readonly Color ProductionColor = new("#4A6E3A"); // deep forest
private static readonly Color DemandColor = new("#B8942A"); // aged gold
private static readonly Color LightColor = new("#F0D9B5");
private static readonly Color DarkColor = new("#B58863");
private static readonly Color WallColor = new("#555555");
private static readonly Color ProductionColor = new("#6B8E5A");
private static readonly Color DemandColor = new("#C9A833");
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;
@ -51,17 +49,6 @@ public partial class CellView : Node2D
};
AddChild(_background);
// Subtle inner shadow for depth (top-left lit, bottom-right dark)
_innerShadow = new ColorRect
{
Size = new Vector2(cellSize, cellSize),
Position = Vector2.Zero,
Color = new Color(0, 0, 0, 0.06f),
Visible = cellType == CellType.Empty,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_innerShadow);
_highlight = new ColorRect
{
Size = new Vector2(cellSize, cellSize),
@ -73,34 +60,54 @@ public partial class CellView : Node2D
AddChild(_highlight);
// Hover outline (4 border rects)
_hoverTop = CreateBorderRect(new Vector2(cellSize, OutlineWidth), Vector2.Zero);
_hoverBottom = CreateBorderRect(new Vector2(cellSize, OutlineWidth), new Vector2(0, cellSize - OutlineWidth));
_hoverLeft = CreateBorderRect(new Vector2(OutlineWidth, cellSize), Vector2.Zero);
_hoverRight = CreateBorderRect(new Vector2(OutlineWidth, cellSize), new Vector2(cellSize - OutlineWidth, 0));
_label = new Label
_hoverTop = new ColorRect
{
Position = new Vector2(4, 3),
Text = "",
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,
Size = new Vector2(cellSize, OutlineWidth),
Position = Vector2.Zero,
Color = HoverOutlineColor,
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(rect);
return rect;
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
{
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;
@ -121,15 +128,14 @@ public partial class CellView : Node2D
}
/// <summary>
/// Production pulse: warm glow that radiates outward.
/// Brief white flash on the cell to signal production.
/// </summary>
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;
var tween = CreateTween();
tween.TweenProperty(_highlight, "color", new Color(0.9f, 0.85f, 0.5f, 0f), duration)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
tween.TweenProperty(_highlight, "color", new Color(1, 1, 1, 0f), duration);
tween.TweenCallback(Callable.From(() => _highlight.Visible = false));
}
}

View file

@ -1,5 +1,4 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Chessistics.Engine.Commands;
@ -38,7 +37,6 @@ public partial class Main : Node2D
private PanelContainer _sidePanel = null!;
private PanelContainer _controlBarWrapper = null!;
private Camera2D _camera = null!;
private ColorRect _fadeOverlay = null!;
// Simulation timer
private Godot.Timer _simTimer = null!;
@ -46,7 +44,7 @@ public partial class Main : Node2D
private bool _running;
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 ControlBarHeight = 48f;
@ -60,9 +58,6 @@ public partial class Main : Node2D
BuildSceneTree();
ConnectSignals();
ShowLevelSelect();
// Fade in from black on startup
FadeIn(0.5f);
}
public override void _UnhandledInput(InputEvent @event)
@ -71,10 +66,6 @@ public partial class Main : Node2D
{
if (mb.ButtonIndex == MouseButton.Middle)
_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)
{
@ -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()
{
// Camera
_camera = new Camera2D { Enabled = true };
AddChild(_camera);
// SFX
var sfx = new SfxManager();
AddChild(sfx);
// Board
_boardView = new BoardView();
AddChild(_boardView);
@ -144,35 +107,14 @@ public partial class Main : Node2D
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore;
_uiLayer.AddChild(uiRoot);
// 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);
// Level title (top-left)
_levelTitle = new Label { Text = "CHESSISTICS" };
_levelTitle.SetAnchorsPreset(Control.LayoutPreset.TopLeft);
_levelTitle.OffsetLeft = 16;
_levelTitle.OffsetTop = 12;
_levelTitle.AddThemeFontSizeOverride("font_size", 20);
_levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore;
titleBar.AddChild(_levelTitle);
uiRoot.AddChild(titleBar);
uiRoot.AddChild(_levelTitle);
// --- Side Panel (anchored to right edge) ---
_sidePanel = new PanelContainer();
@ -262,15 +204,6 @@ public partial class Main : Node2D
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_levelSelectScreen);
// --- Fade overlay (on top of everything) ---
_fadeOverlay = new ColorRect
{
Color = new Color(0, 0, 0, 1),
MouseFilter = Control.MouseFilterEnum.Ignore
};
_fadeOverlay.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_fadeOverlay);
// Initialize animator
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
}
@ -300,24 +233,13 @@ public partial class Main : Node2D
var snap = _sim.GetSnapshot();
if (snap.Phase != SimPhase.Edit) return;
_boardView.ClearHighlights();
var coords = new Coords(col, row);
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
if (piece != null)
{
_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
{
_detailPanel.Hide();
}
}
// --- Level Management ---
@ -332,15 +254,8 @@ public partial class Main : Node2D
private void OnLevelSelected(int levelIndex)
{
SfxManager.Instance?.PlayClick();
_currentLevelIndex = levelIndex;
// Fade out, load, fade in
FadeOut(0.25f, () =>
{
LoadLevel(levelIndex);
FadeIn(0.3f);
});
}
private void LoadLevel(int index)
@ -455,13 +370,17 @@ public partial class Main : Node2D
private void CreatePieceVisual(PiecePlacedEvent placed)
{
SfxManager.Instance?.PlayPlace();
var pieceView = new PieceView();
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
_boardView.AddChild(pieceView);
var color = 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();
trajectView.Setup(placed.PieceId,
@ -534,32 +453,13 @@ public partial class Main : Node2D
_running = false;
_simTimer.Stop();
_sim.ProcessCommand(new StopSimulationCommand());
// 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);
}
_eventAnimator.ResetPiecePositions(_sim.GetSnapshot());
_controlBar.UpdateForPhase(SimPhase.Edit);
_controlBar.ResetTurn();
_metricsOverlay.Hide();
_inputMapper.SetSnapshot(snap);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
// Reset objective panel
if (_currentLevel != null)
_objectivePanel.Setup(_currentLevel.Demands);
}
@ -612,18 +512,4 @@ public partial class Main : Node2D
else
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
{
private Sprite2D _shadow = null!;
private Sprite2D _sprite = null!;
private ColorRect _cargoIndicator = null!;
private Label _label = null!;
private Tween? _cargoPulseTween;
public int PieceId { get; private set; }
public PieceKind Kind { get; private set; }
public Coords StartCell { get; private set; }
public Coords EndCell { get; private set; }
// Muted, earthy palette — teal, sienna, gold tones
private static readonly Color PawnColor = new("#5A8C6B"); // sage green
private static readonly Color RookColor = new("#3D6B8E"); // deep teal
private static readonly Color BishopColor = new("#8E5A6B"); // dusty rose
private static readonly Color KnightColor = new("#8E7A3D"); // burnt sienna
private static readonly Color 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);
private static readonly Color PawnColor = new("#7AB54A");
private static readonly Color RookColor = new("#4A7AB5");
private static readonly Color BishopColor = new("#B54A8E");
private static readonly Color KnightColor = new("#B5824A");
private static readonly Color WoodCargoColor = new("#8B6914");
private static readonly Color StoneCargoColor = new("#808080");
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);
var color = GetPieceColor(kind);
// Shadow (slightly offset, rendered first)
_shadow = new Sprite2D();
var shadowTex = new GradientTexture2D
var color = kind switch
{
Width = 44, Height = 44,
Fill = GradientTexture2D.FillEnum.Radial,
Gradient = new Gradient()
PieceKind.Pawn => PawnColor,
PieceKind.Rook => RookColor,
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();
var texture = new GradientTexture2D
{
Width = 48, Height = 48,
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];
texture.Gradient.SetColor(0, color);
texture.Gradient.SetColor(1, color.Darkened(0.3f));
_sprite.Texture = texture;
AddChild(_sprite);
@ -78,7 +63,6 @@ public partial class PieceView : Node2D
PieceKind.Rook => "T",
PieceKind.Bishop => "F",
PieceKind.Knight => "C",
PieceKind.Queen => "D",
_ => "?"
},
HorizontalAlignment = HorizontalAlignment.Center,
@ -87,47 +71,25 @@ public partial class PieceView : Node2D
MouseFilter = Control.MouseFilterEnum.Ignore
};
_label.AddThemeFontSizeOverride("font_size", 16);
_label.AddThemeColorOverride("font_color", new Color(1, 1, 1, 0.9f));
_label.AddThemeColorOverride("font_color", Colors.White);
AddChild(_label);
// Cargo indicator (hidden by default)
_cargoIndicator = new ColorRect
{
Size = new Vector2(12, 12),
Position = new Vector2(-6, -28),
Size = new Vector2(14, 14),
Position = new Vector2(-7, -30),
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_cargoIndicator);
// Entrance animation: scale bounce
Scale = Vector2.Zero;
var entranceTween = CreateTween();
entranceTween.TweenProperty(this, "scale", new Vector2(1.15f, 1.15f), 0.12f)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
entranceTween.TweenProperty(this, "scale", Vector2.One, 0.08f)
.SetEase(Tween.EaseType.InOut);
}
public static Color GetPieceColor(PieceKind kind) => kind switch
{
PieceKind.Pawn => PawnColor,
PieceKind.Rook => RookColor,
PieceKind.Bishop => BishopColor,
PieceKind.Knight => KnightColor,
PieceKind.Queen => QueenColor,
_ => Colors.White
};
public void SetCargo(CargoType? cargo)
{
_cargoPulseTween?.Kill();
_cargoPulseTween = null;
if (cargo == null)
{
_cargoIndicator.Visible = false;
_cargoIndicator.Scale = Vector2.One;
return;
}
@ -138,16 +100,6 @@ public partial class PieceView : Node2D
CargoType.Stone => StoneCargoColor,
_ => Colors.White
};
// Cargo pulse: gentle breathing
_cargoPulseTween = CreateTween();
_cargoPulseTween.SetLoops();
_cargoPulseTween.TweenProperty(_cargoIndicator, "scale",
new Vector2(1.25f, 1.25f), 0.5f)
.SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine);
_cargoPulseTween.TweenProperty(_cargoIndicator, "scale",
Vector2.One, 0.5f)
.SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine);
}
public void AnimateMoveTo(Vector2 target, float duration = 0.3f)
@ -155,6 +107,7 @@ public partial class PieceView : Node2D
var tween = CreateTween();
if (Kind == PieceKind.Knight)
{
// Arc animation for knight
var mid = (Position + target) / 2 + new Vector2(0, -30);
tween.TweenMethod(Callable.From<float>(t =>
{
@ -166,8 +119,7 @@ public partial class PieceView : Node2D
}
else
{
tween.TweenProperty(this, "position", target, duration)
.SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(this, "position", target, duration);
}
}
}

View file

@ -5,34 +5,15 @@ namespace Chessistics.Scripts.Pieces;
public partial class TrajectView : Line2D
{
public int PieceId { get; private set; }
private Polygon2D? _arrow;
public void Setup(int pieceId, Vector2 from, Vector2 to, Color color)
{
PieceId = pieceId;
Width = 2.5f;
DefaultColor = new Color(color, 0.35f);
Antialiased = true;
Width = 3f;
DefaultColor = new Color(color, 0.5f);
ClearPoints();
AddPoint(from);
AddPoint(to);
ZIndex = -1;
// Arrowhead at the end point
var dir = (to - from).Normalized();
var perp = new Vector2(-dir.Y, dir.X);
float arrowSize = 8f;
var tip = to - dir * 4f; // slightly inset from end
var baseL = tip - dir * arrowSize + perp * arrowSize * 0.5f;
var baseR = tip - dir * arrowSize - perp * arrowSize * 0.5f;
_arrow = new Polygon2D
{
Polygon = [tip - Position, baseL - Position, baseR - Position],
Color = new Color(color, 0.4f),
Position = Vector2.Zero
};
// Position relative to parent, not this Line2D
AddChild(_arrow);
}
}

View file

@ -23,14 +23,14 @@ public partial class EventAnimator : Node
private bool _animating;
public bool IsAnimating => _animating;
private static readonly Color WoodCargoColor = new("#A67C32");
private static readonly Color StoneCargoColor = new("#7A7A7A");
private static readonly Color WoodCargoColor = new("#8B6914");
private static readonly Color StoneCargoColor = new("#808080");
private const float ProduceDuration = 0.35f;
private const float TransferDuration = 0.28f;
private const float MoveDuration = 0.32f;
private const float KnightMoveDuration = 0.42f;
private const float DestroyDuration = 0.45f;
private const float ProduceDuration = 0.3f;
private const float TransferDuration = 0.25f;
private const float MoveDuration = 0.3f;
private const float KnightMoveDuration = 0.4f;
private const float DestroyDuration = 0.3f;
[Signal]
public delegate void TurnAnimationCompletedEventHandler();
@ -107,8 +107,6 @@ public partial class EventAnimator : Node
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
tween.TweenCallback(Callable.From(() =>
{
SfxManager.Instance?.PlayVictory();
SpawnConfetti();
_metricsOverlay.ShowMetrics(victory.Metrics);
EmitSignal(SignalName.VictoryReached);
}));
@ -139,50 +137,45 @@ public partial class EventAnimator : Node
List<PieceMovedEvent> moveEvents,
List<PieceDestroyedEvent> collisionEvents)
{
// Phase 1: Produce — warm golden flash + particle burst
// Phase 1: Produce — flash production cells
if (produceEvents.Count > 0)
{
var captured = produceEvents.ToList();
tween.TweenCallback(Callable.From(() =>
{
SfxManager.Instance?.PlayProduce();
foreach (var evt in captured)
foreach (var evt in produceEvents.ToList())
{
var cell = _boardView.GetCellView(evt.ProductionCell);
cell?.FlashProduce(ProduceDuration);
SpawnProduceParticles(evt.ProductionCell, evt.Type);
}
}));
tween.TweenInterval(ProduceDuration);
produceEvents.Clear();
}
// Phase 2: Transfers — cargo slides with trail particles
// Phase 2: Transfers — animate cargo sliding from giver to receiver
if (transferEvents.Count > 0)
{
// Capture the events list before clearing
var eventsToAnimate = transferEvents.ToList();
// Step 1: remove cargo from givers + spawn sliding cargo sprites
tween.TweenCallback(Callable.From(() =>
{
bool hasDelivery = false;
foreach (var evt in eventsToAnimate)
{
if (evt is CargoTransferredEvent transfer)
{
// Remove cargo indicator from giver
if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value))
_pieceViews[transfer.GivingPieceId.Value].SetCargo(null);
// Create sliding cargo sprite
SpawnCargoSlide(transfer);
if (transfer.ReceivingPieceId == null) hasDelivery = true;
}
}
if (hasDelivery)
SfxManager.Instance?.PlayDeliver();
else
SfxManager.Instance?.PlayTransfer();
}));
// Step 2: wait for slide, then show cargo on receivers + update demand progress
tween.TweenInterval(TransferDuration);
tween.TweenCallback(Callable.From(() =>
{
@ -203,10 +196,9 @@ public partial class EventAnimator : Node
transferEvents.Clear();
}
// Phase 3: Movement — simultaneous, with sfx
// Phase 3: Movement — all pieces move simultaneously
if (moveEvents.Count > 0)
{
tween.TweenCallback(Callable.From(() => SfxManager.Instance?.PlayMove()));
tween.SetParallel(true);
foreach (var moved in moveEvents)
{
@ -214,218 +206,74 @@ public partial class EventAnimator : Node
{
var target = _boardView.CoordsToPixel(moved.To);
float duration = pv.Kind == PieceKind.Knight ? KnightMoveDuration : MoveDuration;
tween.TweenProperty(pv, "position", target, duration)
.SetEase(Tween.EaseType.InOut).SetTrans(Tween.TransitionType.Sine);
tween.TweenProperty(pv, "position", target, duration);
}
}
tween.SetParallel(false);
moveEvents.Clear();
}
// Phase 4: Collision/Destruction — shrink + spin + particles
// Phase 4: Collision/Destruction
if (collisionEvents.Count > 0)
{
var captured = collisionEvents.ToList();
tween.SetParallel(true);
foreach (var destroyed in collisionEvents)
{
var pieceId = destroyed.PieceId;
tween.TweenCallback(Callable.From(() =>
{
SfxManager.Instance?.PlayDestroy();
foreach (var destroyed in captured)
{
if (_pieceViews.TryGetValue(destroyed.PieceId, out var pv))
{
SpawnDestroyParticles(pv.Position);
var dt = pv.CreateTween();
dt.SetParallel(true);
dt.TweenProperty(pv, "scale", Vector2.Zero, DestroyDuration)
.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Back);
dt.TweenProperty(pv, "rotation", Mathf.Pi * 2f, DestroyDuration);
dt.TweenProperty(pv, "modulate:a", 0f, DestroyDuration);
}
}
FlashPiece(pieceId);
UnregisterPiece(pieceId);
}));
}
tween.SetParallel(false);
tween.TweenInterval(DestroyDuration);
tween.TweenCallback(Callable.From(() =>
{
foreach (var destroyed in captured)
UnregisterPiece(destroyed.PieceId);
}));
collisionEvents.Clear();
}
}
// --- Visual Effects ---
/// <summary>
/// Creates a temporary colored square that slides from the giver to the receiver.
/// </summary>
private void SpawnCargoSlide(CargoTransferredEvent transfer)
{
var from = _boardView.CoordsToPixel(transfer.From);
var to = _boardView.CoordsToPixel(transfer.To);
var color = GetCargoColor(transfer.Type);
var container = new Node2D { Position = from };
_boardView.AddChild(container);
// Main cargo square
var main = new ColorRect
{
Size = new Vector2(12, 12),
Position = new Vector2(-6, -6),
Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore
};
container.AddChild(main);
// Trail particles (2 smaller squares that follow with delay)
for (int i = 1; i <= 2; i++)
{
float trailDelay = i * 0.04f;
float trailSize = 12f - i * 3f;
float trailAlpha = 0.6f - i * 0.2f;
var trail = new ColorRect
{
Size = new Vector2(trailSize, trailSize),
Position = new Vector2(-trailSize / 2f, -trailSize / 2f),
Color = new Color(color, trailAlpha),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var trailContainer = new Node2D { Position = from };
trailContainer.AddChild(trail);
_boardView.AddChild(trailContainer);
var trailTween = trailContainer.CreateTween();
trailTween.TweenInterval(trailDelay);
trailTween.TweenProperty(trailContainer, "position", to, TransferDuration - trailDelay)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
trailTween.TweenCallback(Callable.From(() => trailContainer.QueueFree()));
}
// Main slide with whip easing
var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
}
private void SpawnProduceParticles(Coords cell, CargoType type)
{
var center = _boardView.CoordsToPixel(cell);
var color = GetCargoColor(type);
var rng = new Random();
for (int i = 0; i < 6; i++)
{
float angle = i * Mathf.Pi * 2f / 6f + (float)rng.NextDouble() * 0.5f;
float dist = 25f + (float)rng.NextDouble() * 15f;
float size = 4f + (float)rng.NextDouble() * 4f;
var particle = new ColorRect
{
Size = new Vector2(size, size),
Position = new Vector2(-size / 2f, -size / 2f),
Color = new Color(color, 0.8f),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var pNode = new Node2D { Position = center };
pNode.AddChild(particle);
_boardView.AddChild(pNode);
var target = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
var pt = pNode.CreateTween();
pt.SetParallel(true);
pt.TweenProperty(pNode, "position", target, ProduceDuration * 0.8f)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
pt.TweenProperty(pNode, "modulate:a", 0f, ProduceDuration);
pt.SetParallel(false);
pt.TweenCallback(Callable.From(() => pNode.QueueFree()));
}
}
private void SpawnDestroyParticles(Vector2 position)
{
var rng = new Random();
var red = new Color(0.9f, 0.25f, 0.2f);
for (int i = 0; i < 10; i++)
{
float angle = (float)rng.NextDouble() * Mathf.Pi * 2f;
float dist = 20f + (float)rng.NextDouble() * 30f;
float size = 3f + (float)rng.NextDouble() * 5f;
var particle = new ColorRect
{
Size = new Vector2(size, size),
Position = new Vector2(-size / 2f, -size / 2f),
Color = new Color(red, 0.9f),
MouseFilter = Control.MouseFilterEnum.Ignore
};
var pNode = new Node2D { Position = position };
pNode.AddChild(particle);
_boardView.AddChild(pNode);
var target = position + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * dist;
var pt = pNode.CreateTween();
pt.SetParallel(true);
pt.TweenProperty(pNode, "position", target, DestroyDuration * 0.7f)
.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
pt.TweenProperty(pNode, "modulate:a", 0f, DestroyDuration);
pt.TweenProperty(pNode, "scale", Vector2.Zero, DestroyDuration)
.SetEase(Tween.EaseType.In);
pt.SetParallel(false);
pt.TweenCallback(Callable.From(() => pNode.QueueFree()));
}
}
private void SpawnConfetti()
{
var rng = new Random();
var viewport = GetViewport().GetVisibleRect().Size;
Color[] confettiColors = [
new("#FFD700"), new("#FF6B35"), new("#3D8E5A"),
new("#4A7AB5"), new("#B54A8E"), new("#E8D5A8")
];
for (int i = 0; i < 40; i++)
{
float x = (float)rng.NextDouble() * viewport.X;
float startY = -20f;
float endY = viewport.Y + 50f;
float size = 4f + (float)rng.NextDouble() * 6f;
float duration = 1.5f + (float)rng.NextDouble() * 1.5f;
float delay = (float)rng.NextDouble() * 0.5f;
float drift = ((float)rng.NextDouble() - 0.5f) * 80f;
var confetti = new ColorRect
{
Size = new Vector2(size, size * 0.5f),
Color = confettiColors[rng.Next(confettiColors.Length)],
MouseFilter = Control.MouseFilterEnum.Ignore
};
// Confetti needs to be in screen space (CanvasLayer)
var parent = GetTree().Root;
var cNode = new Node2D { Position = new Vector2(x, startY) };
cNode.AddChild(confetti);
parent.AddChild(cNode);
var ct = cNode.CreateTween();
ct.TweenInterval(delay);
ct.SetParallel(true);
ct.TweenProperty(cNode, "position", new Vector2(x + drift, endY), duration)
.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Sine);
ct.TweenProperty(cNode, "rotation", (float)rng.NextDouble() * Mathf.Pi * 4f, duration);
ct.SetParallel(false);
ct.TweenCallback(Callable.From(() => cNode.QueueFree()));
}
}
private static Color GetCargoColor(CargoType type) => type switch
var color = transfer.Type switch
{
CargoType.Wood => WoodCargoColor,
CargoType.Stone => StoneCargoColor,
_ => Colors.White
};
var sprite = new ColorRect
{
Size = new Vector2(14, 14),
Position = new Vector2(-7, -7),
Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore
};
var container = new Node2D { Position = from };
container.AddChild(sprite);
_boardView.AddChild(container);
var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Cubic);
slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
}
private void FlashPiece(int pieceId)
{
if (!_pieceViews.TryGetValue(pieceId, out var pv)) return;
var tween = pv.CreateTween();
tween.TweenProperty(pv, "modulate", new Color(1, 0.2f, 0.2f), 0.1f);
tween.TweenProperty(pv, "modulate", Colors.White, 0.1f);
tween.SetLoops(3);
}
public void ResetPiecePositions(BoardSnapshot snapshot)
{
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 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()
{
AddThemeConstantOverride("separation", 8);
_playButton = CreateStyledButton("PLAY");
_playButton = new Button { Text = "▶ PLAY" };
_playButton.Pressed += () => EmitSignal(SignalName.PlayPressed);
AddChild(_playButton);
_pauseButton = CreateStyledButton("PAUSE");
_pauseButton = new Button { Text = "⏸ PAUSE" };
_pauseButton.Pressed += () => EmitSignal(SignalName.PausePressed);
AddChild(_pauseButton);
_stepButton = CreateStyledButton("STEP");
_stepButton = new Button { Text = "⏭ STEP" };
_stepButton.Pressed += () => EmitSignal(SignalName.StepPressed);
AddChild(_stepButton);
_stopButton = CreateStyledButton("STOP");
_stopButton = new Button { Text = "⏹ STOP" };
_stopButton.Pressed += () => EmitSignal(SignalName.StopPressed);
AddChild(_stopButton);
// Spacer
AddChild(new Control { CustomMinimumSize = new Vector2(12, 0) });
_speedSelect = new OptionButton { CustomMinimumSize = new Vector2(60, 30) };
_speedSelect = new OptionButton();
_speedSelect.AddItem("x1", 0);
_speedSelect.AddItem("x2", 1);
_speedSelect.AddItem("x4", 2);
_speedSelect.ItemSelected += OnSpeedSelected;
AddChild(_speedSelect);
// Spacer
AddChild(new Control { SizeFlagsHorizontal = SizeFlags.ExpandFill });
_turnLabel = new Label { Text = "Coup: --" };
_turnLabel.AddThemeFontSizeOverride("font_size", 13);
_turnLabel.AddThemeColorOverride("font_color", new Color("#999999"));
_turnLabel.AddThemeFontSizeOverride("font_size", 14);
AddChild(_turnLabel);
UpdateForPhase(SimPhase.Edit);
}
private static Button CreateStyledButton(string text)
{
var btn = new Button
{
Text = text,
CustomMinimumSize = new Vector2(70, 30)
};
btn.AddThemeFontSizeOverride("font_size", 11);
var normal = MakeStyle(BtnBg);
var hover = MakeStyle(BtnHover);
var pressed = MakeStyle(BtnPressed);
var disabled = MakeStyle(BtnDisabled);
disabled.BorderColor = new Color("#2A2A2E");
btn.AddThemeStyleboxOverride("normal", normal);
btn.AddThemeStyleboxOverride("hover", hover);
btn.AddThemeStyleboxOverride("pressed", pressed);
btn.AddThemeStyleboxOverride("disabled", disabled);
btn.AddThemeColorOverride("font_disabled_color", new Color("#555555"));
return btn;
}
private static StyleBoxFlat MakeStyle(Color bg)
{
return new StyleBoxFlat
{
BgColor = bg,
BorderColor = BtnBorder,
BorderWidthBottom = 1, BorderWidthTop = 1,
BorderWidthLeft = 1, BorderWidthRight = 1,
CornerRadiusTopLeft = 4, CornerRadiusTopRight = 4,
CornerRadiusBottomLeft = 4, CornerRadiusBottomRight = 4,
ContentMarginLeft = 10, ContentMarginRight = 10,
ContentMarginTop = 4, ContentMarginBottom = 4
};
}
private void OnSpeedSelected(long index)
{
float speed = index switch

View file

@ -1,6 +1,5 @@
using Godot;
using Chessistics.Engine.Model;
using Chessistics.Scripts.Pieces;
namespace Chessistics.Scripts.UI;
@ -15,41 +14,17 @@ public partial class DetailPanel : PanelContainer
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();
vbox.AddThemeConstantOverride("separation", 4);
var title = new Label { Text = "DETAIL" };
title.AddThemeFontSizeOverride("font_size", 13);
title.AddThemeColorOverride("font_color", new Color("#B8942A"));
title.AddThemeFontSizeOverride("font_size", 14);
vbox.AddChild(title);
_infoLabel = new Label { Text = "" };
_infoLabel.AddThemeFontSizeOverride("font_size", 11);
_infoLabel.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
vbox.AddChild(_infoLabel);
_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 = new Button { Text = "Retirer" };
_removeButton.Pressed += () => EmitSignal(SignalName.RemoveRequested, _currentPieceId);
vbox.AddChild(_removeButton);
@ -62,11 +37,9 @@ public partial class DetailPanel : PanelContainer
_currentPieceId = piece.Id;
var kindName = piece.Kind switch
{
PieceKind.Pawn => "Pion",
PieceKind.Rook => "Tour II",
PieceKind.Bishop => "Fou II",
PieceKind.Knight => "Cavalier",
PieceKind.Queen => "Dame",
_ => 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 Carrefour", "Deux productions, deux demandes, et un carrefour au centre."),
("Le Labyrinthe", "Un couloir etroit serpente a travers les murs."),
("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.")
("Trois Royaumes", "Trois productions, trois demandes. Gerez un reseau complet.")
];
public override void _Ready()

View file

@ -10,62 +10,46 @@ public partial class MetricsOverlay : PanelContainer
[Signal]
public delegate void RetryPressedEventHandler();
private Label _titleLabel = null!;
private Label _piecesLabel = null!;
private Label _turnsLabel = null!;
private Label _cellsLabel = null!;
private HBoxContainer _buttons = null!;
private Label _metricsLabel = null!;
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();
vbox.AddThemeConstantOverride("separation", 8);
vbox.SetAnchorsPreset(LayoutPreset.Center);
_titleLabel = new Label
var title = new Label
{
Text = "VICTOIRE !",
HorizontalAlignment = HorizontalAlignment.Center
};
_titleLabel.AddThemeFontSizeOverride("font_size", 26);
_titleLabel.AddThemeColorOverride("font_color", new Color("#FFD700"));
vbox.AddChild(_titleLabel);
title.AddThemeFontSizeOverride("font_size", 24);
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
vbox.AddChild(title);
vbox.AddChild(new HSeparator());
_piecesLabel = CreateMetricLabel();
vbox.AddChild(_piecesLabel);
_turnsLabel = CreateMetricLabel();
vbox.AddChild(_turnsLabel);
_cellsLabel = CreateMetricLabel();
vbox.AddChild(_cellsLabel);
_metricsLabel = new Label
{
Text = "",
HorizontalAlignment = HorizontalAlignment.Center
};
_metricsLabel.AddThemeFontSizeOverride("font_size", 14);
vbox.AddChild(_metricsLabel);
vbox.AddChild(new HSeparator());
_buttons = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.Center };
_buttons.AddThemeConstantOverride("separation", 16);
var buttons = new HBoxContainer();
buttons.Alignment = BoxContainer.AlignmentMode.Center;
var retryBtn = CreateStyledButton("Rejouer");
var retryBtn = new Button { Text = "Rejouer" };
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);
_buttons.AddChild(nextBtn);
buttons.AddChild(nextBtn);
vbox.AddChild(_buttons);
vbox.AddChild(buttons);
AddChild(vbox);
Visible = false;
@ -73,87 +57,11 @@ public partial class MetricsOverlay : PanelContainer
public void ShowMetrics(Metrics metrics)
{
_piecesLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}";
_turnsLabel.Text = $"Coups: {metrics.TurnsTaken}";
_cellsLabel.Text = $"Cases occupees: {metrics.CellsOccupied}";
// Start invisible, fade + scale in
Modulate = new Color(1, 1, 1, 0);
Scale = new Vector2(0.85f, 0.85f);
PivotOffset = Size / 2f;
_metricsLabel.Text = $"Pieces utilisees: {metrics.PiecesUsed}\n" +
$"Coups: {metrics.TurnsTaken}\n" +
$"Cases occupees: {metrics.CellsOccupied}";
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;
}
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;
}
public new void Hide() => Visible = false;
}

View file

@ -6,7 +6,7 @@ namespace Chessistics.Scripts.UI;
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)
{
@ -16,7 +16,7 @@ public partial class ObjectivePanel : VBoxContainer
var title = new Label { Text = "OBJECTIFS" };
title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#B8942A")); // aged gold
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title);
AddChild(new HSeparator());
@ -24,11 +24,9 @@ public partial class ObjectivePanel : VBoxContainer
foreach (var demand in demands)
{
var vbox = new VBoxContainer();
vbox.AddThemeConstantOverride("separation", 2);
var label = new Label { Text = $"{demand.Name}: 0/{demand.Amount} {demand.Cargo}" };
label.AddThemeFontSizeOverride("font_size", 12);
label.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
vbox.AddChild(label);
var bar = new ProgressBar
@ -36,34 +34,18 @@ public partial class ObjectivePanel : VBoxContainer
MinValue = 0,
MaxValue = demand.Amount,
Value = 0,
CustomMinimumSize = new Vector2(180, 14),
CustomMinimumSize = new Vector2(180, 16),
ShowPercentage = false
};
// Style the progress bar
var bgStyle = new StyleBoxFlat
{
BgColor = new Color(0.2f, 0.2f, 0.22f),
CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3,
CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3
};
var fillStyle = new StyleBoxFlat
{
BgColor = new Color("#3D6B8E"), // teal fill
CornerRadiusTopLeft = 3, CornerRadiusTopRight = 3,
CornerRadiusBottomLeft = 3, CornerRadiusBottomRight = 3
};
bar.AddThemeStyleboxOverride("background", bgStyle);
bar.AddThemeStyleboxOverride("fill", fillStyle);
vbox.AddChild(bar);
var deadline = new Label { Text = $"Deadline: {demand.Deadline} coups" };
deadline.AddThemeFontSizeOverride("font_size", 10);
deadline.AddThemeColorOverride("font_color", new Color("#777777"));
deadline.AddThemeColorOverride("font_color", new Color("#AAAAAA"));
vbox.AddChild(deadline);
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;
entry.label.Text = $"{name}: {current}/{required}";
// 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);
entry.bar.Value = current;
if (current >= required)
{
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);
}
entry.label.AddThemeColorOverride("font_color", new Color("#44CC44"));
}
}

View file

@ -2,7 +2,6 @@ using Godot;
using System;
using System.Collections.Generic;
using Chessistics.Engine.Model;
using Chessistics.Scripts.Pieces;
namespace Chessistics.Scripts.UI;
@ -16,13 +15,6 @@ public partial class PieceStockPanel : VBoxContainer
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)
{
foreach (var child in GetChildren())
@ -32,7 +24,7 @@ public partial class PieceStockPanel : VBoxContainer
var title = new Label { Text = "PIECES" };
title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#B8942A"));
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title);
AddChild(new HSeparator());
@ -40,31 +32,16 @@ public partial class PieceStockPanel : VBoxContainer
foreach (var entry in stock)
{
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
{
Text = GetPieceName(entry.Kind),
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}" };
countLabel.AddThemeFontSizeOverride("font_size", 13);
countLabel.AddThemeColorOverride("font_color", new Color("#999999"));
countLabel.AddThemeFontSizeOverride("font_size", 14);
var kind = entry.Kind;
button.Pressed += () => OnPieceButtonPressed(kind);
@ -95,45 +72,11 @@ public partial class PieceStockPanel : VBoxContainer
{
foreach (var (k, (button, _, remaining)) in _entries)
{
bool selected = k == _selectedKind;
button.ButtonPressed = k == _selectedKind;
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)
{
if (!_entries.TryGetValue(kind, out var entry)) return;
@ -154,7 +97,6 @@ public partial class PieceStockPanel : VBoxContainer
PieceKind.Rook => "Tour II",
PieceKind.Bishop => "Fou II",
PieceKind.Knight => "Cavalier",
PieceKind.Queen => "Dame",
_ => kind.ToString()
};
}

View file

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

View file

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

View file

@ -8,7 +8,6 @@ public static class PieceRules
PieceKind.Rook => 5,
PieceKind.Bishop => 3,
PieceKind.Knight => 3,
PieceKind.Queen => 7,
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
@ -18,7 +17,6 @@ public static class PieceRules
PieceKind.Rook => 2,
PieceKind.Bishop => 2,
PieceKind.Knight => 0, // Knight uses L-shape, not range
PieceKind.Queen => 2,
_ => 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)[] 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 =
[
(1, 2), (2, 1), (2, -1), (1, -2),
@ -24,7 +23,6 @@ public static class MoveValidator
PieceKind.Rook => GetSlidingMoves(start, OrthogonalDirs, 2, board),
PieceKind.Bishop => GetSlidingMoves(start, DiagonalDirs, 2, board),
PieceKind.Knight => GetKnightMoves(start, board),
PieceKind.Queen => GetSlidingMoves(start, AllDirs, 2, board),
_ => []
};
}

View file

@ -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
- 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).
### 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) :
Dame 7
Tour 5
Fou 3
Cavalier 3