things/GunPreviewController.cs

Static controller that spawns a temporary 3D gun preview GameObject above the local player using the current loadout selections, including charms and gems. It creates a root, positions it relative to the camera or world up, clones the gun prefab and attaches charm and gem prefabs to the gun, and provides Show/Hide/Refresh. Also contains a small rotator Component (currently inactive).

File Access
using System.Linq;

/// <summary>
/// Manages a temporary 3D gun preview shown above the lobby when the loadout panel is open.
/// </summary>
public static class GunPreviewController
{
	static GameObject _previewRoot;

	/// <summary>
	/// Spawn the preview gun using the player's current loadout selections.
	/// Safe to call if a preview is already showing — it will be replaced.
	/// </summary>
	public static void Show()
	{
		Hide();

		var player = Manager.Instance?.LocalPlayer;
		if ( player == null || !player.IsValid() ) return;

		var gunId = ProgressManager.GetEffectiveSelectedGunId();
		var gunPrefab = ProgressManager.GetPrefabPath( gunId );
		if ( string.IsNullOrEmpty( gunPrefab ) )
			gunPrefab = "prefabs/guns/gun_default.prefab";

		var charmIds = ProgressManager.GetEffectiveSelectedCharmIds();
		var charm0 = charmIds.Count > 0 ? ProgressManager.GetPrefabPath( charmIds[0] ) ?? "" : "";
		var charm1 = charmIds.Count > 1 ? ProgressManager.GetPrefabPath( charmIds[1] ) ?? "" : "";

		var equippedGems = ProgressManager.GetEffectiveEquippedGems();
		string GetGem( int i ) => i < equippedGems.Count ? ProgressManager.GetPrefabPath( equippedGems[i] ) ?? "" : "";

		// Create a root object positioned above the player in screen space
		_previewRoot = new GameObject( "GunPreview" );
		_previewRoot.Enabled = true;

		float HEIGHT_OFFSET = 130f;

		var cam = Game.ActiveScene.Camera;
		if ( cam != null )
		{
			// Move the preview upward on-screen (camera-relative "up") so it appears above the panel
			var screenUp = cam.WorldRotation.Up;
			_previewRoot.WorldPosition = player.WorldPosition + screenUp * HEIGHT_OFFSET;
		}
		else
		{
			_previewRoot.WorldPosition = player.WorldPosition + Vector3.Up * HEIGHT_OFFSET;
		}

		_previewRoot.Components.Create<GunPreviewRotator>();

		// Clone the gun prefab
		var gunObj = GameObject.Clone( gunPrefab, new CloneConfig { StartEnabled = true } );
		if ( gunObj == null )
		{
			Log.Warning( "GunPreviewController: failed to clone gun prefab " + gunPrefab );
			_previewRoot.Destroy();
			_previewRoot = null;
			return;
		}
		gunObj.SetParent( _previewRoot );
		gunObj.LocalTransform = new Transform( Vector3.Zero, Rotation.FromYaw( -90f ), Vector3.One );

		SpawnCharmonGun( gunObj, charm0, charm1 );
		SpawnGemsOnGun( gunObj, GetGem( 0 ), GetGem( 1 ), GetGem( 2 ), GetGem( 3 ) );
	}

	/// <summary>Destroy the preview gun, if any.</summary>
	public static void Hide()
	{
		if ( _previewRoot != null && _previewRoot.IsValid() )
			_previewRoot.Destroy();
		_previewRoot = null;
	}

	/// <summary>Destroy and re-create the preview using current selections. Call after any loadout change.</summary>
	public static void Refresh()
	{
		Hide();
		Show();
	}

	static void SpawnCharmonGun( GameObject gunObj, string charm0, string charm1 )
	{
		var gun = gunObj.GetComponent<Gun>();
		if ( gun == null ) return;

		var anchors = gun.GetCharmAnchors();
		var paths = new[] { charm0, charm1 };
		for ( int i = 0; i < anchors.Count && i < paths.Length; i++ )
		{
			if ( string.IsNullOrEmpty( paths[i] ) || anchors[i] == null ) continue;
			var charmObj = GameObject.Clone( paths[i], new CloneConfig { StartEnabled = true } );
			if ( charmObj == null ) continue;
			charmObj.SetParent( anchors[i] );
			charmObj.LocalTransform = new Transform( Vector3.Zero, Rotation.Identity, Vector3.One );
		}
	}

	static void SpawnGemsOnGun( GameObject gunObj, string gem0, string gem1, string gem2, string gem3 )
	{
		var gun = gunObj.GetComponent<Gun>();
		if ( gun == null || gun.GemSlots == null ) return;

		var gemPaths = new[] { gem0, gem1, gem2, gem3 };
		for ( int i = 0; i < gun.GemSlots.Count && i < gemPaths.Length; i++ )
		{
			if ( string.IsNullOrEmpty( gemPaths[i] ) ) continue;
			var slot = gun.GemSlots[i];
			if ( slot == null ) continue;
			var gemObj = GameObject.Clone( gemPaths[i], new CloneConfig { StartEnabled = true } );
			if ( gemObj == null ) { Log.Warning( $"GunPreviewController: gem prefab not found: {gemPaths[i]}" ); continue; }
			gemObj.SetParent( slot );
			gemObj.LocalTransform = new Transform( Vector3.Zero, Rotation.Identity, Vector3.One );
		}
	}
}

/// <summary>Slowly rotates the gun preview around the world-up axis for a display effect.</summary>
public class GunPreviewRotator : Component
{
	protected override void OnUpdate()
	{
		//WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Up, Time.Delta * 60f );
	}
}