From c4f6ecbf44fab4229ac66f3618643727d2a9b9b8 Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Fri, 17 Apr 2026 22:21:36 +0200 Subject: [PATCH] Add collision camera pan/zoom and toast notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventAnimator now emits CollisionOccurred at the end of the collision phase, carrying the struck cell and victim/destroyer identity. Main pans and zooms the camera onto the cell over 0.45s and shows a fading toast ("Pion détruit par Tour — retourné au stock", or "collision mutuelle" for same-status ties). The toast fades out after 3s and leaves the camera framing the collision so the player can inspect the aftermath before resuming. --- Scripts/Main.cs | 61 +++++++++++++++++++++++++++ Scripts/Presentation/EventAnimator.cs | 8 ++++ docs/PLAN.md | 6 --- tools/automation/test_collision.py | 35 +++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 tools/automation/test_collision.py diff --git a/Scripts/Main.cs b/Scripts/Main.cs index 7043d7f..7a8e7d4 100644 --- a/Scripts/Main.cs +++ b/Scripts/Main.cs @@ -40,6 +40,7 @@ public partial class Main : Node2D private Camera2D _camera = null!; private ColorRect _fadeOverlay = null!; private FlavorBanner _flavorBanner = null!; + private Label _collisionToast = null!; // Simulation timer private Godot.Timer _simTimer = null!; @@ -394,6 +395,27 @@ public partial class Main : Node2D _titleScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect); uiRoot.AddChild(_titleScreen); + // --- Collision toast (hidden by default, shown on collision) --- + _collisionToast = new Label + { + Text = "", + Visible = false, + MouseFilter = Control.MouseFilterEnum.Ignore + }; + _collisionToast.AddThemeFontSizeOverride("font_size", 14); + _collisionToast.AddThemeColorOverride("font_color", new Color("#FFE8D0")); + _collisionToast.AddThemeColorOverride("font_outline_color", new Color(0, 0, 0, 0.8f)); + _collisionToast.AddThemeConstantOverride("outline_size", 4); + _collisionToast.AnchorLeft = 0.0f; + _collisionToast.AnchorRight = 0.0f; + _collisionToast.AnchorTop = 1.0f; + _collisionToast.AnchorBottom = 1.0f; + _collisionToast.OffsetLeft = 20; + _collisionToast.OffsetRight = 400; + _collisionToast.OffsetTop = -ControlBarHeight - 60; + _collisionToast.OffsetBottom = -ControlBarHeight - 20; + uiRoot.AddChild(_collisionToast); + // --- Flavor Banner (narrative text) --- _flavorBanner = new FlavorBanner(); _flavorBanner.AnchorLeft = 0.1f; @@ -430,6 +452,7 @@ public partial class Main : Node2D _eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted; _eventAnimator.VictoryReached += OnCampaignComplete; _eventAnimator.MissionAdvanced += OnMissionAdvanced; + _eventAnimator.CollisionOccurred += OnCollisionOccurred; _metricsOverlay.NextLevelPressed += OnBackToMenu; _detailPanel.RemoveRequested += OnRemoveRequested; _inputMapper.CellClicked += OnCellClicked; @@ -871,6 +894,44 @@ public partial class Main : Node2D } } + private void OnCollisionOccurred(int col, int row, string victimKind, int destroyerId) + { + if (_sim == null) return; + + // Pan + slight zoom to the collision cell + var cellPixel = _boardView.CoordsToPixel(new Coords(col, row)); + var originalZoom = _camera.Zoom; + var targetZoom = originalZoom * 1.4f; + var tween = CreateTween(); + tween.SetParallel(true); + tween.TweenProperty(_camera, "position", cellPixel, 0.45f) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); + tween.TweenProperty(_camera, "zoom", targetZoom, 0.45f) + .SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic); + + // Compose toast message + string message; + if (destroyerId < 0) + { + message = $"{victimKind} détruit (collision mutuelle) — retourné au stock"; + } + else + { + var snap = _sim.GetSnapshot(); + var dp = snap.Pieces.FirstOrDefault(p => p.Id == destroyerId); + var destroyer = dp?.Kind.ToString() ?? "?"; + message = $"{victimKind} détruit par {destroyer} — retourné au stock"; + } + _collisionToast.Text = message; + _collisionToast.Modulate = new Color(1, 1, 1, 0); + _collisionToast.Visible = true; + var fadeIn = _collisionToast.CreateTween(); + fadeIn.TweenProperty(_collisionToast, "modulate:a", 1f, 0.2f); + fadeIn.TweenInterval(3.0f); + fadeIn.TweenProperty(_collisionToast, "modulate:a", 0f, 0.5f); + fadeIn.TweenCallback(Callable.From(() => _collisionToast.Visible = false)); + } + private void OnUndo() { if (_sim == null) return; diff --git a/Scripts/Presentation/EventAnimator.cs b/Scripts/Presentation/EventAnimator.cs index 750f3b5..141ed7b 100644 --- a/Scripts/Presentation/EventAnimator.cs +++ b/Scripts/Presentation/EventAnimator.cs @@ -41,6 +41,8 @@ public partial class EventAnimator : Node public delegate void VictoryReachedEventHandler(); [Signal] public delegate void MissionAdvancedEventHandler(); + [Signal] + public delegate void CollisionOccurredEventHandler(int col, int row, string victimKind, int destroyerId); public void Initialize(BoardView boardView, ObjectivePanel objectivePanel, ControlBar controlBar, MetricsOverlay metricsOverlay) @@ -268,6 +270,12 @@ public partial class EventAnimator : Node dt.TweenProperty(pv, "modulate:a", 0f, DestroyDuration); } } + + // Emit signal for Main to pan camera + show toast + var first = captured[0]; + EmitSignal(SignalName.CollisionOccurred, + first.Cell.Col, first.Cell.Row, first.Kind.ToString(), + first.DestroyerPieceId ?? -1); })); tween.TweenInterval(DestroyDuration); tween.TweenCallback(Callable.From(() => diff --git a/docs/PLAN.md b/docs/PLAN.md index 9475490..2191759 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,12 +12,6 @@ et l'extension de la campagne. Le moteur expose deja les commandes et events requis ; cote Godot il manque les surfaces d'interaction et d'animation. -### 1.2 Drag & drop des pieces placees -`MovePieceCommand` + `PieceMovedByPlayerEvent` existent cote engine. -Cote Godot : permettre de glisser une piece placee pour deplacer son point de -depart. Les arrivees legales se recalculent pendant le drag. Application -atomique au relachement (entre deux tours). - ### 1.3 Collision — camera pan/zoom + notification L'engine emet deja `PieceReturnedToStockEvent` + auto-pause. Il reste a : - Animer un pan + zoom de la camera vers la case de collision. diff --git a/tools/automation/test_collision.py b/tools/automation/test_collision.py new file mode 100644 index 0000000..d4f713c --- /dev/null +++ b/tools/automation/test_collision.py @@ -0,0 +1,35 @@ +"""Smoke test for collision camera pan + notification toast.""" +import sys, time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from tools.automation.harness import Harness + + +def main(): + with Harness.launch(run_name="collision") as h: + h.load_mission("campaign_01", 0) + + # Two Pawns that will collide on (1,0) at turn 1 — mutual destruction + h.place("Pawn", (0, 0), (1, 0)) + h.place("Pawn", (2, 0), (1, 0)) + h.screenshot("01_before_collision") + + h.set_speed(0.2) + h.play() + time.sleep(1.5) + + h.screenshot("02_during_pan_zoom") + time.sleep(1.0) + h.screenshot("03_toast_visible") + + s = h.state() + print(f"[after] phase={s['phase']} pieces={len(s['pieces'])} stock={s['remainingStock']}") + assert s['phase'] == 'Paused', f"Expected auto-pause after collision, got {s['phase']}" + assert len(s['pieces']) == 0, "Both pawns should have been returned to stock" + print("OK — collision auto-pause, pieces returned to stock") + + +if __name__ == "__main__": + main()