Add collision camera pan/zoom and toast notification

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.
This commit is contained in:
Samuel Bouchet 2026-04-17 22:21:36 +02:00
parent 1522b70398
commit c4f6ecbf44
4 changed files with 104 additions and 6 deletions

View file

@ -40,6 +40,7 @@ public partial class Main : Node2D
private Camera2D _camera = null!; private Camera2D _camera = null!;
private ColorRect _fadeOverlay = null!; private ColorRect _fadeOverlay = null!;
private FlavorBanner _flavorBanner = null!; private FlavorBanner _flavorBanner = null!;
private Label _collisionToast = null!;
// Simulation timer // Simulation timer
private Godot.Timer _simTimer = null!; private Godot.Timer _simTimer = null!;
@ -394,6 +395,27 @@ public partial class Main : Node2D
_titleScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect); _titleScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_titleScreen); 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) --- // --- Flavor Banner (narrative text) ---
_flavorBanner = new FlavorBanner(); _flavorBanner = new FlavorBanner();
_flavorBanner.AnchorLeft = 0.1f; _flavorBanner.AnchorLeft = 0.1f;
@ -430,6 +452,7 @@ public partial class Main : Node2D
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted; _eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
_eventAnimator.VictoryReached += OnCampaignComplete; _eventAnimator.VictoryReached += OnCampaignComplete;
_eventAnimator.MissionAdvanced += OnMissionAdvanced; _eventAnimator.MissionAdvanced += OnMissionAdvanced;
_eventAnimator.CollisionOccurred += OnCollisionOccurred;
_metricsOverlay.NextLevelPressed += OnBackToMenu; _metricsOverlay.NextLevelPressed += OnBackToMenu;
_detailPanel.RemoveRequested += OnRemoveRequested; _detailPanel.RemoveRequested += OnRemoveRequested;
_inputMapper.CellClicked += OnCellClicked; _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() private void OnUndo()
{ {
if (_sim == null) return; if (_sim == null) return;

View file

@ -41,6 +41,8 @@ public partial class EventAnimator : Node
public delegate void VictoryReachedEventHandler(); public delegate void VictoryReachedEventHandler();
[Signal] [Signal]
public delegate void MissionAdvancedEventHandler(); public delegate void MissionAdvancedEventHandler();
[Signal]
public delegate void CollisionOccurredEventHandler(int col, int row, string victimKind, int destroyerId);
public void Initialize(BoardView boardView, ObjectivePanel objectivePanel, public void Initialize(BoardView boardView, ObjectivePanel objectivePanel,
ControlBar controlBar, MetricsOverlay metricsOverlay) ControlBar controlBar, MetricsOverlay metricsOverlay)
@ -268,6 +270,12 @@ public partial class EventAnimator : Node
dt.TweenProperty(pv, "modulate:a", 0f, DestroyDuration); 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.TweenInterval(DestroyDuration);
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>

View file

@ -12,12 +12,6 @@ et l'extension de la campagne.
Le moteur expose deja les commandes et events requis ; cote Godot il manque Le moteur expose deja les commandes et events requis ; cote Godot il manque
les surfaces d'interaction et d'animation. 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 ### 1.3 Collision — camera pan/zoom + notification
L'engine emet deja `PieceReturnedToStockEvent` + auto-pause. Il reste a : L'engine emet deja `PieceReturnedToStockEvent` + auto-pause. Il reste a :
- Animer un pan + zoom de la camera vers la case de collision. - Animer un pan + zoom de la camera vers la case de collision.

View file

@ -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()