Loot/LootContainer.cs

A scene-system usable component that spawns a loot container GameObject and, when used, animates an open state and ejects a burst of Loot prefabs with physics impulses. It chooses a model based on current level, spawns loot items, scales and sets rarity, and drives a replicated 'Spitting' flag so all clients animate the lid.

NetworkingFile Access
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Sandbox;

namespace BrickJam;

/// <summary>
/// A lockable container that, when opened, spits out a burst of loot. Scene-System port of the
/// legacy <c>LootContainer : UsableEntity</c>.
///
/// Deferred: the open animation parameter and the lock object/lockpicking (legacy <c>Lock</c>)
/// belong to the interactions system; only the replicated <see cref="LegacyUsableComponent.Locked"/>
/// flag is set here.
/// </summary>
[Title( "Loot Container" )]
[Category( "Loot" )]
public sealed partial class LootContainer : LegacyUsableComponent
{
	[Sync] public bool Spitting { get; set; }

	public override bool CanUse => !Spitting;
	public override string UseString { get => CanUse ? "open the container" : string.Empty; set { } }
	public override string LockText => "lockpick the container";
	public override bool StartLocked => true;
	public override float InteractionDuration { get => 2f; set { } }

	private static readonly IReadOnlyDictionary<LevelType, string> Models = new Dictionary<LevelType, string>
	{
		[LevelType.Mansion] = "models/containers/safe/safe.vmdl",
		[LevelType.Dungeon] = "models/containers/chest/chest.vmdl",
		[LevelType.Bathrooms] = "models/containers/medicine_cabinet/medicine_cabinet.vmdl"
	};

	public static LootContainer Create( Vector3 position, Rotation rotation )
	{
		var level = MansionGame.Instance?.CurrentLevelType ?? LevelType.Mansion;
		if ( !Models.TryGetValue( level, out var modelPath ) )
		{
			Log.Warning( "Failed to spawn loot container!!" );
			return null;
		}

		var go = new GameObject( true, "LootContainer" );
		go.WorldPosition = position;
		go.WorldRotation = rotation;
		go.Tags.Add( "solid", "container", "usable" );

		var model = Model.Load( modelPath );
		var renderer = go.Components.Create<SkinnedModelRenderer>();
		renderer.Model = model;

		var collider = go.Components.Create<ModelCollider>();
		collider.Model = model;

		var container = go.Components.Create<LootContainer>();
		container.Locked = container.StartLocked;

		go.NetworkSpawn();
		return container;
	}

	private SkinnedModelRenderer bodyRenderer;

	protected override void OnStart()
	{
		base.OnStart();
		bodyRenderer = Components.Get<SkinnedModelRenderer>();
	}

	protected override void OnUpdate()
	{
		// Drive the lid-open animation off the replicated Spitting flag so EVERY client animates it
		// (Spit() runs host-only, so a host-side SetAnimParameter wouldn't reach clients). Legacy set the
		// "open" param directly in spit().
		bodyRenderer?.Set( "open", Spitting );
	}

	public override void Use( Player user )
	{
		if ( Spitting || !Networking.IsHost )
			return;

		_ = Spit();
	}

	private async Task Spit()
	{
		Spitting = true;

		var level = MansionGame.Instance?.CurrentLevelType ?? LevelType.Mansion;
		var lootCount = MansionGame.Random.Int( 2, 5 );
		var levelLoot = LootPrefab.All.Where( x => x.Value.Level == level ).Select( x => x.Value ).ToArray();
		var fallback = LootPrefab.All.FirstOrDefault().Value;

		for ( var i = 0; i < lootCount; i++ )
		{
			var normal = (WorldRotation.Forward + Vector3.Random / 8f).WithZ( 0 );
			var prefab = MansionGame.Random.FromArray( levelLoot, fallback );
			var loot = Loot.Create( prefab, WorldPosition, MansionGame.Random.Rotation() );

			if ( loot is not null )
			{
				loot.GameObject.WorldScale = Vector3.One * 0.01f;
				loot.Rarity = (LootRarity)Math.Min( (int)loot.Rarity + 2, 8 );

				if ( loot.Components.TryGet<Rigidbody>( out var body ) )
				{
					body.MotionEnabled = true;
					// Set velocity directly (mass-independent) for a gentle pop out in front of the
					// container - ApplyImpulse scaled by 1/mass launched low-mass loot way too hard.
					body.Velocity = normal * 140f + Vector3.Up * 300f;
				}
			}

			await Task.DelayRealtimeSeconds( 1.5f / lootCount );
		}
	}
}