MenuManager.cs
using Sandbox;
using System.Collections.Generic;

/// <summary>
/// Drives the pre-game flow: Menu (camera snapped to MenuStartAnchor,
/// menu overlaid on top) -> fade out -> camera pans to the gameplay
/// position -> Playing. RoundManager and GameHud both check this
/// component's State so they stay dormant/hidden until the pan finishes.
/// </summary>
public sealed class MenuManager : Component
{
	public enum MenuState
	{
		Menu,
		FadingOut,
		Transitioning,
		Playing
	}

	[Property] public GameObject Camera { get; set; }
	[Property] public GameObject MenuStartAnchor { get; set; }
	[Property] public GameObject PlayCameraAnchor { get; set; }

	// How long the menu overlay takes to fade out (matches the CSS
	// transition in MenuHud.razor.scss - keep the two roughly in sync).
	[Property] public float FadeOutDuration { get; set; } = 0.8f;

	[Property] public float PanDuration { get; set; } = 2.5f;

	// --- Audio Properties ---
	[Property] public List<Component> AmbientSounds { get; set; }
	[Property] public bool StartMuted { get; set; } = false;
	
	[Property] public List<Component> TalkingSounds { get; set; }
	[Property] public bool StartTalkingMuted { get; set; } = false;

	public MenuState State { get; private set; } = MenuState.Menu;
	public bool IsMuted { get; private set; }
	public bool IsTalkingMuted { get; private set; }

	// Wherever the camera was ORIGINALLY authored in the scene, before
	// this script snaps it out to MenuStartAnchor. Used as the pan
	// destination if PlayCameraAnchor isn't set.
	private Vector3 _originalPosition;
	private Rotation _originalRotation;

	private Vector3 _panStartPos;
	private Rotation _panStartRot;
	private Vector3 _panEndPos;
	private Rotation _panEndRot;

	private TimeSince _fadeTimer;
	private TimeSince _panTimer;

	protected override void OnStart()
	{
		Mouse.Visibility = MouseVisibility.Visible;

		if ( Camera is not null )
		{
			_originalPosition = Camera.Transform.Position;
			_originalRotation = Camera.Transform.Rotation;

			if ( MenuStartAnchor is not null )
			{
				Camera.Transform.Position = MenuStartAnchor.Transform.Position;
				Camera.Transform.Rotation = MenuStartAnchor.Transform.Rotation;
			}
		}

		IsMuted = StartMuted;
		ApplyMuteState();
		
		IsTalkingMuted = StartTalkingMuted;
		ApplyTalkingMuteState();
	}

	protected override void OnUpdate()
	{
		if ( State == MenuState.FadingOut && _fadeTimer > FadeOutDuration )
		{
			BeginPan();
			return;
		}

		if ( State == MenuState.Transitioning && Camera is not null )
		{
			var t = System.MathF.Min( 1f, _panTimer / System.MathF.Max( 0.01f, PanDuration ) );

			// Smoothstep - eases in and out instead of a linear pan.
			var eased = t * t * (3f - 2f * t);

			Camera.Transform.Position = Vector3.Lerp( _panStartPos, _panEndPos, eased );
			Camera.Transform.Rotation = Rotation.Slerp( _panStartRot, _panEndRot, eased );

			if ( t >= 1f )
			{
				State = MenuState.Playing;
				Mouse.Visibility = MouseVisibility.Hidden;
			}
		}
	}

	// Called from MenuHud.razor's Play button. Starts the fade - the
	// actual camera pan begins once FadeOutDuration has elapsed, so the
	// overlay is fully gone before the camera starts moving.
	public void StartGame()
	{
		if ( State != MenuState.Menu )
			return;

		State = MenuState.FadingOut;
		_fadeTimer = 0;
	}

	private void BeginPan()
	{
		State = MenuState.Transitioning;
		_panTimer = 0;

		_panStartPos = Camera?.Transform.Position ?? Vector3.Zero;
		_panStartRot = Camera?.Transform.Rotation ?? Rotation.Identity;

		if ( PlayCameraAnchor is not null )
		{
			_panEndPos = PlayCameraAnchor.Transform.Position;
			_panEndRot = PlayCameraAnchor.Transform.Rotation;
		}
		else
		{
			// No explicit destination set - pan back to wherever the
			// camera was originally authored before we snapped it to
			// MenuStartAnchor.
			_panEndPos = _originalPosition;
			_panEndRot = _originalRotation;
		}
	}

	// --- Ambient Music Mute Logic ---
	public void ToggleMute()
	{
		IsMuted = !IsMuted;
		ApplyMuteState();
	}

	private void ApplyMuteState()
	{
		if ( AmbientSounds is null ) return;

		foreach ( var sound in AmbientSounds )
		{
			if ( sound is null ) continue;

			if ( sound is SoundscapeTrigger trigger )
			{
				trigger.Volume = IsMuted ? 0f : 1f;
			}
			else 
			{
				sound.Enabled = !IsMuted;
			}
		}
	}

	// --- Talking Audio Mute Logic ---
	public void ToggleTalkingMute()
	{
		IsTalkingMuted = !IsTalkingMuted;
		ApplyTalkingMuteState();
	}

	private void ApplyTalkingMuteState()
	{
		if ( TalkingSounds is null ) return;

		foreach ( var sound in TalkingSounds )
		{
			if ( sound is null ) continue;

			if ( sound is SoundscapeTrigger trigger )
			{
				trigger.Volume = IsTalkingMuted ? 0f : 1f;
			}
			else 
			{
				sound.Enabled = !IsTalkingMuted;
			}
		}
	}
}