A networked world loot component for the game. Creates a GameObject with renderer, collider and rigidbody, shows a local world label, handles player Use to pick up items, applies impulse, moves into player inventory and spawns effects/log messages.
using System;
using Sandbox;
namespace BrickJam;
/// <summary>
/// A pickup-able loot item in the world. Scene-System port of the legacy <c>Loot : UsableEntity</c>.
///
/// The legacy entity built its own model/physics; here a loot is a GameObject with a
/// <see cref="ModelRenderer"/> + <see cref="ModelCollider"/> + <see cref="Rigidbody"/> created at
/// runtime by <see cref="Create"/>.
///
/// Deferred: the ground-loot world UI panel (<c>GroundLootPanel</c>) and event-log messages belong
/// to the UI system. The pickup itself works against the ported <see cref="ContainerComponent"/>.
/// </summary>
[Title( "Loot" )]
[Category( "Loot" )]
public sealed partial class Loot : LegacyUsableComponent
{
[Property, Sync] public string PrefabName { get; set; }
[Sync] public LootRarity Rarity { get; set; } = LootRarity.Common;
[Sync] public int BaseMonetaryValue { get; set; }
[Sync] public string BaseName { get; set; } = "Loot";
[Sync] public Player LastPlayer { get; set; }
public LootPrefab Prefab => LootPrefab.Get( PrefabName );
public string FullName => $"{Rarity} {BaseName}";
public int MonetaryValue => (int)(BaseMonetaryValue * LootRarityTable.RarityMap[Rarity]);
public override string UseString { get => $"take the {FullName}"; set { } }
public override float InteractionDuration { get => 0.2f; set { } }
private Player picker;
private bool deleting;
public static Loot Create( LootPrefab resource, Vector3 position, Rotation rotation, bool setRarity = true )
{
if ( resource is null )
return null;
var go = new GameObject( true, "Loot" );
go.WorldPosition = position;
go.WorldRotation = rotation;
go.Tags.Add( "loot", "usable" );
var model = Model.Load( string.IsNullOrEmpty( resource.Model ) ? "models/error.vmdl" : resource.Model );
var renderer = go.Components.Create<ModelRenderer>();
renderer.Model = model;
var collider = go.Components.Create<ModelCollider>();
collider.Model = model;
var body = go.Components.Create<Rigidbody>();
body.MotionEnabled = false; // keyframed until picked up
var loot = go.Components.Create<Loot>();
loot.PrefabName = resource.ResourceName;
loot.BaseMonetaryValue = resource.MonetaryValue;
loot.BaseName = resource.Name;
if ( setRarity )
loot.Rarity = LootRarityTable.RandomRarityFromLevel( MansionGame.Instance?.CurrentLevelType ?? LevelType.Mansion );
go.NetworkSpawn();
return loot;
}
/// <summary>
/// Build the world-anchored loot label LOCALLY on every client. World-space razor panels must be created
/// locally (not baked into the networked object), because a <c>PanelComponent</c> recreated from the
/// network snapshot on a proxy never rebuilds its razor tree - it would only render on the host. The
/// label reads the loot's [Sync]'d Rarity/BaseName, so it shows correctly on each client.
/// </summary>
private GameObject labelGo;
private const float LabelRange = 300f;
protected override void OnStart()
{
base.OnStart();
labelGo = new GameObject( true, "Label" ) { Parent = GameObject };
labelGo.LocalPosition = Vector3.Up * 20f;
labelGo.NetworkMode = NetworkMode.Never;
var panel = labelGo.Components.Create<WorldPanel>();
panel.LookAtCamera = true;
panel.PanelSize = new Vector2( 1500, 300 );
var type = Game.TypeLibrary.GetType( "GroundLootPanel" );
if ( type is not null )
labelGo.Components.Create( type );
}
protected override void OnUpdate()
{
// A WorldPanel re-renders every frame, so with lots of loot on the ground all those labels were a
// constant Render/UI cost. Only render this one while the local player is near it.
if ( !labelGo.IsValid() )
return;
var show = Player.Local.IsValid() && Player.Local.WorldPosition.Distance( WorldPosition ) <= LabelRange;
if ( labelGo.Enabled != show )
labelGo.Enabled = show;
}
public static Loot CreateFromEntry( ItemEntry entry, Vector3 position, Rotation rotation )
{
var loot = Create( entry.Prefab, position, rotation, false );
if ( loot is not null )
loot.Rarity = entry.Rarity;
return loot;
}
public override void Use( Player user )
{
if ( !Networking.IsHost || picker is not null )
return;
picker = user;
LastPlayer = user;
if ( Components.TryGet<Rigidbody>( out var body ) )
{
body.MotionEnabled = true;
var normal = (user.EyePosition - WorldPosition).Normal;
var force = 100f + WorldPosition.Distance( user.EyePosition );
body.ApplyImpulse( force * (normal + Vector3.Up * 0.5f) );
}
SoundExtensions.BroadcastPlay( "sounds/grab/grab.sound", WorldPosition );
}
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
if ( picker is null )
{
GameObject.WorldScale = Vector3.Lerp( GameObject.WorldScale, Vector3.One, 5f * Time.Delta );
return;
}
GameObject.WorldScale = Vector3.Lerp( GameObject.WorldScale, Vector3.One * 0.01f, 5f * Time.Delta );
if ( GameObject.WorldScale.x.AlmostEqual( 0.01f, 0.1f ) && !deleting )
{
deleting = true;
var inventory = picker.Components.Get<ContainerComponent>();
if ( inventory is not null && inventory.Add( new ItemEntry { Prefab = Prefab, Rarity = Rarity } ) )
{
MansionGame.Instance?.ShowEventlog( $"You picked up <gray>1x {FullName}." );
MansionGame.Instance?.PlayEffect( "prefabs/particles/smoke_steal.prefab", WorldPosition, Rotation.Identity );
picker.TrackStat( GameStats.LootCollected, 1 ); // routed to the picker's own client
GameObject.Destroy();
}
else
{
MansionGame.Instance?.ShowEventlog( $"<red>No space for <gray>1x {FullName}." );
picker = null;
deleting = false;
}
}
}
}