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.
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 );
}
}
}