using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Chessistics.Engine.Commands;
using Chessistics.Engine.Events;
using Chessistics.Engine.Loading;
using Chessistics.Engine.Model;
using Chessistics.Engine.Simulation;
using Chessistics.Scripts.Automation;
using Chessistics.Scripts.Board;
using Chessistics.Scripts.Input;
using Chessistics.Scripts.Pieces;
using Chessistics.Scripts.Presentation;
using Chessistics.Scripts.UI;
namespace Chessistics.Scripts;
public partial class Main : Node2D
{
private GameSim? _sim;
private CampaignDef? _campaignDef;
// Views
private BoardView _boardView = null!;
private InputMapper _inputMapper = null!;
private EventAnimator _eventAnimator = null!;
// UI
private CanvasLayer _uiLayer = null!;
private ObjectivePanel _objectivePanel = null!;
private PieceStockPanel _pieceStockPanel = null!;
private DetailPanel _detailPanel = null!;
private ControlBar _controlBar = null!;
private MetricsOverlay _metricsOverlay = null!;
private LevelSelectScreen _titleScreen = null!;
private Label _levelTitle = null!;
private PanelContainer _sidePanel = null!;
private PanelContainer _controlBarWrapper = null!;
private Camera2D _camera = null!;
private ColorRect _fadeOverlay = null!;
private FlavorBanner _flavorBanner = null!;
// Simulation timer
private Godot.Timer _simTimer = null!;
private float _simInterval = 1.0f;
private bool _running;
private bool _panning;
private bool _rightDragged;
private bool _collisionPauseOccurred;
private const float CameraKeyboardSpeed = 400f;
private const float SidePanelWidth = 280f;
private const float ControlBarHeight = 48f;
private const float TitleBarHeight = 40f;
private static readonly Color BackgroundColor = new("#2D2D2D");
// Automation harness (active only when --automation=
CLI flag is given)
private string? _automationDir;
private AutomationHarness? _automationHarness;
public override void _Ready()
{
RenderingServer.SetDefaultClearColor(BackgroundColor);
_automationDir = ParseAutomationArg();
BuildSceneTree();
ConnectSignals();
ShowTitleScreen();
if (_automationDir != null)
{
// Skip the opening fade so the harness sees a stable frame immediately.
_fadeOverlay.Color = new Color(0, 0, 0, 0);
MountAutomationHarness(_automationDir);
}
else
{
FadeIn(0.5f);
}
}
private static string? ParseAutomationArg()
{
foreach (var arg in OS.GetCmdlineArgs())
{
if (arg.StartsWith("--automation=", StringComparison.Ordinal))
return arg.Substring("--automation=".Length);
}
return null;
}
private void MountAutomationHarness(string dir)
{
var facade = new AutomationFacade(
sim: () => _sim,
input: _inputMapper,
animator: _eventAnimator,
stock: _pieceStockPanel,
controlBar: _controlBar,
loadMission: HarnessLoadMission,
play: OnPlay,
pause: OnPause,
step: OnStep,
togglePlayPause: TogglePlayPause,
backToMenu: HarnessBackToMenu,
setSpeed: HarnessSetSpeed,
quit: () => GetTree().Quit());
_automationHarness = new AutomationHarness(dir, facade);
AddChild(_automationHarness);
}
private void HarnessLoadMission(string campaignName, int missionIndex)
{
var path = $"res://Data/campaigns/{campaignName}.json";
LoadCampaignDirect(path);
// Fast-forward to the requested mission by completing prior ones synthetically.
while (_sim != null && _campaignDef != null
&& _sim.GetSnapshot().Campaign is { } campSnap
&& campSnap.CurrentMissionIndex < missionIndex)
{
_sim.ProcessCommand(new PauseSimulationCommand()); // no-op if already paused
// Cheat the phase to MissionComplete then Advance — only valid in automation.
// Simpler: refuse non-zero missionIndex for now and require AdvanceMission
// via gameplay. Breaking the break keeps harness predictable.
GD.PrintErr($"[Automation] missionIndex > 0 not supported yet; staying at mission {campSnap.CurrentMissionIndex}");
break;
}
}
private void HarnessBackToMenu()
{
_running = false;
_simTimer.Stop();
_eventAnimator.ClearAll();
ShowTitleScreen();
}
private void HarnessSetSpeed(float interval)
{
_simInterval = interval;
OnSpeedChanged(interval);
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseButton mb)
{
if (mb.ButtonIndex == MouseButton.Middle)
{
_panning = mb.Pressed;
}
else if (mb.ButtonIndex == MouseButton.Right)
{
if (mb.Pressed)
{
_panning = true;
_rightDragged = false;
}
else
{
_panning = false;
if (!_rightDragged)
{
_inputMapper.Cancel();
_pieceStockPanel.ClearSelection();
}
}
}
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)
{
_rightDragged = true;
_camera.Position -= motion.Relative / _camera.Zoom;
}
else if (@event is InputEventKey key && key.Pressed && !key.Echo && key.Keycode == Key.Space)
{
TogglePlayPause();
GetViewport().SetInputAsHandled();
}
}
public override void _Process(double delta)
{
var dir = Vector2.Zero;
if (Godot.Input.IsKeyPressed(Key.Z) || Godot.Input.IsKeyPressed(Key.W))
dir.Y -= 1;
if (Godot.Input.IsKeyPressed(Key.S))
dir.Y += 1;
if (Godot.Input.IsKeyPressed(Key.Q) || Godot.Input.IsKeyPressed(Key.A))
dir.X -= 1;
if (Godot.Input.IsKeyPressed(Key.D))
dir.X += 1;
if (dir != Vector2.Zero)
_camera.Position += dir.Normalized() * CameraKeyboardSpeed * (float)delta / _camera.Zoom.X;
}
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 = new Camera2D { Enabled = true };
AddChild(_camera);
var sfx = new SfxManager();
AddChild(sfx);
_boardView = new BoardView();
AddChild(_boardView);
_inputMapper = new InputMapper();
_inputMapper.Initialize(_boardView);
AddChild(_inputMapper);
_eventAnimator = new EventAnimator();
AddChild(_eventAnimator);
_simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval };
_simTimer.Timeout += OnSimTimerTick;
AddChild(_simTimer);
// --- UI Layer ---
_uiLayer = new CanvasLayer();
AddChild(_uiLayer);
var uiRoot = new Control();
uiRoot.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore;
_uiLayer.AddChild(uiRoot);
// Title bar
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.AddThemeFontSizeOverride("font_size", 20);
_levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore;
titleBar.AddChild(_levelTitle);
uiRoot.AddChild(titleBar);
// --- Side Panel ---
_sidePanel = new PanelContainer();
_sidePanel.AnchorLeft = 1.0f;
_sidePanel.AnchorRight = 1.0f;
_sidePanel.AnchorTop = 0.0f;
_sidePanel.AnchorBottom = 1.0f;
_sidePanel.OffsetLeft = -SidePanelWidth;
_sidePanel.OffsetRight = 0;
_sidePanel.OffsetTop = 0;
_sidePanel.OffsetBottom = -ControlBarHeight;
var sidePanelStyle = new StyleBoxFlat
{
BgColor = new Color(0.14f, 0.14f, 0.16f, 0.97f),
BorderColor = new Color(0.25f, 0.25f, 0.28f),
BorderWidthLeft = 1,
ContentMarginLeft = 16, ContentMarginRight = 16,
ContentMarginTop = 16, ContentMarginBottom = 16
};
_sidePanel.AddThemeStyleboxOverride("panel", sidePanelStyle);
var sideScroll = new ScrollContainer
{
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled,
SizeFlagsVertical = Control.SizeFlags.ExpandFill
};
var sideVBox = new VBoxContainer();
sideVBox.AddThemeConstantOverride("separation", 12);
_objectivePanel = new ObjectivePanel();
sideVBox.AddChild(_objectivePanel);
sideVBox.AddChild(new HSeparator());
_pieceStockPanel = new PieceStockPanel();
sideVBox.AddChild(_pieceStockPanel);
_detailPanel = new DetailPanel();
sideVBox.AddChild(_detailPanel);
sideScroll.AddChild(sideVBox);
_sidePanel.AddChild(sideScroll);
uiRoot.AddChild(_sidePanel);
// --- Control Bar ---
_controlBarWrapper = new PanelContainer();
_controlBarWrapper.AnchorLeft = 0.0f;
_controlBarWrapper.AnchorRight = 1.0f;
_controlBarWrapper.AnchorTop = 1.0f;
_controlBarWrapper.AnchorBottom = 1.0f;
_controlBarWrapper.OffsetTop = -ControlBarHeight;
_controlBarWrapper.OffsetRight = -SidePanelWidth;
var controlBarStyle = new StyleBoxFlat
{
BgColor = new Color(0.11f, 0.11f, 0.13f, 0.97f),
BorderColor = new Color(0.25f, 0.25f, 0.28f),
BorderWidthTop = 1,
ContentMarginLeft = 12, ContentMarginRight = 12,
ContentMarginTop = 4, ContentMarginBottom = 4
};
_controlBarWrapper.AddThemeStyleboxOverride("panel", controlBarStyle);
_controlBar = new ControlBar();
_controlBarWrapper.AddChild(_controlBar);
uiRoot.AddChild(_controlBarWrapper);
// --- Metrics Overlay ---
var metricsCenter = new CenterContainer();
metricsCenter.SetAnchorsPreset(Control.LayoutPreset.FullRect);
metricsCenter.OffsetRight = -SidePanelWidth;
metricsCenter.OffsetBottom = -ControlBarHeight;
metricsCenter.MouseFilter = Control.MouseFilterEnum.Ignore;
_metricsOverlay = new MetricsOverlay();
_metricsOverlay.CustomMinimumSize = new Vector2(340, 260);
metricsCenter.AddChild(_metricsOverlay);
uiRoot.AddChild(metricsCenter);
// --- Title Screen ---
_titleScreen = new LevelSelectScreen();
_titleScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_titleScreen);
// --- Flavor Banner (narrative text) ---
_flavorBanner = new FlavorBanner();
_flavorBanner.AnchorLeft = 0.1f;
_flavorBanner.AnchorRight = 0.7f;
_flavorBanner.AnchorTop = 0.0f;
_flavorBanner.AnchorBottom = 0.0f;
_flavorBanner.OffsetTop = 44; // Below title bar
_flavorBanner.OffsetBottom = 100;
uiRoot.AddChild(_flavorBanner);
// --- Fade overlay ---
_fadeOverlay = new ColorRect
{
Color = new Color(0, 0, 0, 1),
MouseFilter = Control.MouseFilterEnum.Ignore
};
_fadeOverlay.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_fadeOverlay);
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
}
private void ConnectSignals()
{
_titleScreen.StartCampaignPressed += OnStartCampaign;
_pieceStockPanel.PieceSelected += OnPieceKindSelected;
_inputMapper.PlacementRequested += OnPlacementRequested;
_inputMapper.Cancelled += OnPlacementCancelled;
_controlBar.PlayPressed += OnPlay;
_controlBar.PausePressed += OnPause;
_controlBar.StepPressed += OnStep;
_controlBar.SpeedChanged += OnSpeedChanged;
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
_eventAnimator.VictoryReached += OnCampaignComplete;
_eventAnimator.MissionAdvanced += OnMissionAdvanced;
_metricsOverlay.NextLevelPressed += OnBackToMenu;
_detailPanel.RemoveRequested += OnRemoveRequested;
_inputMapper.CellClicked += OnCellClicked;
}
private void OnCellClicked(int col, int row)
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
_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);
var pieceColor = PieceView.GetPieceColor(piece.Kind);
var highlightColor = new Color(pieceColor, 0.3f);
_boardView.HighlightCells([piece.StartCell, piece.EndCell], highlightColor);
}
else
{
_detailPanel.Hide();
}
}
// --- Campaign Management ---
private void ShowTitleScreen()
{
_titleScreen.Visible = true;
_boardView.Visible = false;
_sidePanel.Visible = false;
_controlBarWrapper.Visible = false;
_levelTitle.Visible = false;
}
private void OnStartCampaign()
{
SfxManager.Instance?.PlayClick();
FadeOut(0.25f, () =>
{
LoadCampaign();
FadeIn(0.3f);
});
}
private void LoadCampaign() => LoadCampaignDirect("res://Data/campaigns/campaign_01.json");
private void LoadCampaignDirect(string path)
{
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read);
if (file == null)
{
GD.PrintErr($"Cannot open campaign file: {path}");
return;
}
var json = file.GetAsText();
file.Close();
_campaignDef = CampaignLoader.Load(json);
_sim = new GameSim(_campaignDef);
// Load campaign: applies mission 0 terrain, stock, unlocked pieces
var loadEvents = _sim.ProcessCommand(new LoadCampaignCommand());
foreach (var evt in loadEvents)
{
if (evt is CommandRejectedEvent r)
{
GD.PrintErr($"Cannot load campaign: {r.Reason}");
return;
}
}
_titleScreen.Visible = false;
_boardView.Visible = true;
_sidePanel.Visible = true;
_controlBarWrapper.Visible = true;
_levelTitle.Visible = true;
var snap = _sim.GetSnapshot();
var mission = _campaignDef.Missions[snap.Campaign!.CurrentMissionIndex];
BuildBoardFromSnapshot(snap);
SetupUIForMission(snap, mission);
CenterCameraOnBoard(snap.Width, snap.Height);
_inputMapper.SetSnapshot(snap);
}
private void BuildBoardFromSnapshot(BoardSnapshot snap)
{
_eventAnimator.ClearAll();
_boardView.BuildBoardFromSnapshot(snap);
// Recreate piece visuals if any exist
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);
}
}
private void SetupUIForMission(BoardSnapshot snap, MissionDef mission)
{
// Show all demands (current + previous missions)
var allDemands = snap.Demands
.Select(d => new DemandDef(d.Position, d.Name, d.Cargo, d.Required))
.ToList();
_objectivePanel.Setup(allDemands);
// Setup stock panel with only unlocked piece kinds
var availableStock = new List();
foreach (var (kind, remaining) in snap.RemainingStock)
{
if (snap.Campaign != null && !snap.Campaign.AvailablePieceKinds.Contains(kind))
continue;
availableStock.Add(new PieceStock(kind, remaining));
}
_pieceStockPanel.Setup(availableStock);
_controlBar.UpdateForPhase(snap.Phase);
_controlBar.ResetTurn();
_metricsOverlay.Hide();
_detailPanel.Hide();
var missionNum = snap.Campaign!.CurrentMissionIndex + 1;
var totalMissions = _campaignDef!.Missions.Count;
_levelTitle.Text = $"CHESSISTICS — Mission {missionNum}/{totalMissions}: {mission.Name}";
// Show narrative flavor text
_flavorBanner.ShowFlavor(mission.Flavor);
}
private void CenterCameraOnBoard(int width, int height)
{
_camera.Position = new Vector2(
width * BoardView.CellSize / 2f,
-height * BoardView.CellSize / 2f + BoardView.CellSize
);
// Offset: shift view to account for side panel (right) and title/control bars
// Positive X → camera looks right → board appears left (compensates right panel)
// Positive Y → camera looks down → board appears up (compensates bottom bar)
_camera.Offset = new Vector2(SidePanelWidth / 2f, (ControlBarHeight - TitleBarHeight) / 2f);
}
// --- Placement ---
private void OnPieceKindSelected(int kindIndex)
{
_inputMapper.SelectPieceKind((PieceKind)kindIndex);
}
private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow)
{
if (_sim == null) return;
var kind = (PieceKind)kindIndex;
var start = new Coords(startCol, startRow);
var end = new Coords(endCol, endRow);
var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end));
HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
_pieceStockPanel.ClearSelection();
}
private void OnPlacementCancelled()
{
_pieceStockPanel.ClearSelection();
}
private void OnRemoveRequested(int pieceId)
{
if (_sim == null) return;
var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId));
HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
}
private void HandleEditEvents(IReadOnlyList events)
{
foreach (var evt in events)
{
switch (evt)
{
case PiecePlacedEvent placed:
CreatePieceVisual(placed);
UpdateStockFromSnapshot();
break;
case PieceRemovedEvent removed:
_eventAnimator.UnregisterPiece(removed.PieceId);
UpdateStockFromSnapshot();
_detailPanel.Hide();
break;
case PlacementRejectedEvent rejected:
GD.Print($"Placement rejected: {rejected.Reason}");
break;
case CommandRejectedEvent rejected:
GD.Print($"Command rejected: {rejected.Reason}");
break;
}
}
}
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 trajectView = new TrajectView();
trajectView.Setup(placed.PieceId,
_boardView.CoordsToPixel(placed.Start),
_boardView.CoordsToPixel(placed.End),
color);
_boardView.AddChild(trajectView);
_eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView);
}
private void UpdateStockFromSnapshot()
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
foreach (var (kind, remaining) in snap.RemainingStock)
_pieceStockPanel.UpdateCount(kind, remaining);
}
// --- Simulation Control ---
private void TogglePlayPause()
{
if (_sim == null) return;
var phase = _sim.GetSnapshot().Phase;
if (phase == SimPhase.Running)
OnPause();
else if (phase == SimPhase.Paused)
OnPlay();
}
private void OnPlay()
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
if (snap.Phase == SimPhase.Paused || snap.Phase == SimPhase.MissionComplete)
{
_sim.ProcessCommand(new ResumeSimulationCommand());
}
_running = true;
_controlBar.UpdateForPhase(SimPhase.Running);
_simTimer.WaitTime = _simInterval;
_simTimer.Start();
}
private void OnPause()
{
if (_sim == null) return;
_sim.ProcessCommand(new PauseSimulationCommand());
_running = false;
_simTimer.Stop();
_controlBar.UpdateForPhase(SimPhase.Paused);
}
private void OnStep()
{
if (_sim == null || _eventAnimator.IsAnimating) return;
_collisionPauseOccurred = false;
var events = _sim.ProcessCommand(new StepSimulationCommand());
// Detect if a collision auto-pause happened this step
_collisionPauseOccurred = events.Any(e => e is PieceReturnedToStockEvent);
_eventAnimator.ProcessEvents(events);
_controlBar.UpdateForPhase(_sim.GetSnapshot().Phase);
}
private void OnSpeedChanged(float interval)
{
_simInterval = interval;
if (_simTimer.TimeLeft > 0)
_simTimer.WaitTime = interval;
}
private void OnSimTimerTick()
{
if (_sim == null || _eventAnimator.IsAnimating) return;
_collisionPauseOccurred = false;
var events = _sim.ProcessCommand(new StepSimulationCommand());
_collisionPauseOccurred = events.Any(e => e is PieceReturnedToStockEvent);
_eventAnimator.ProcessEvents(events);
}
private void OnTurnAnimationCompleted()
{
if (_sim == null) return;
var phase = _sim.GetSnapshot().Phase;
_controlBar.UpdateForPhase(phase);
if (phase == SimPhase.MissionComplete)
{
// Stop auto-running, show mission complete overlay
_running = false;
_simTimer.Stop();
}
else if (_collisionPauseOccurred && _running)
{
// Collision caused auto-pause — stop the timer
_running = false;
_simTimer.Stop();
_collisionPauseOccurred = false;
}
// Otherwise: if _running, the timer will fire next step automatically
}
private void OnCampaignComplete()
{
// Last mission complete — show victory overlay
_running = false;
_simTimer.Stop();
if (_sim == null || _campaignDef == null) return;
var snap = _sim.GetSnapshot();
_metricsOverlay.ShowMissionComplete(
snap.Campaign!.CurrentMissionIndex + 1,
snap.TurnNumber,
true
);
}
private void OnMissionAdvanced()
{
// Auto-advance happened during simulation — rebuild board seamlessly
if (_sim == null || _campaignDef == null) return;
var snap = _sim.GetSnapshot();
var mission = _campaignDef.Missions[snap.Campaign!.CurrentMissionIndex];
BuildBoardFromSnapshot(snap);
SetupUIForMission(snap, mission);
CenterCameraOnBoard(snap.Width, snap.Height);
_inputMapper.SetSnapshot(snap);
}
// --- Navigation ---
private void OnBackToMenu()
{
SfxManager.Instance?.PlayClick();
_running = false;
_simTimer.Stop();
_eventAnimator.ClearAll();
FadeOut(0.2f, () =>
{
ShowTitleScreen();
FadeIn(0.3f);
});
}
}