Code/Tikfinity/Actions/TikfinitySpawnPrefabAction.cs
using System.Threading.Tasks;
using Sandbox;

namespace TikfinitySbox;

/// <summary>
/// No-code route action that spawns a bounded number of prefab instances on the host.
/// </summary>
public sealed class TikfinitySpawnPrefabAction : Component, ITikfinityEvents
{
    [Property] public string Route { get; set; } = "gift.spawn";
    [Property] public GameObject Prefab { get; set; }
    [Property] public GameObject SpawnPoint { get; set; }
    [Property] public bool NetworkSpawnedObjects { get; set; } = true;
    [Property, Range( 1, 100 )] public int MaxSpawnsPerInteraction { get; set; } = 10;
    [Property, Range( 0f, 3600f )] public float LifetimeSeconds { get; set; }
    [Property, Range( 0f, 1000f )] public float RandomRadius { get; set; }

    void ITikfinityEvents.OnInteraction( TikfinityInteraction interaction )
    {
        if ( !Networking.IsHost || interaction is null ) return;
        if ( !string.Equals( Route?.Trim(), interaction.Route, System.StringComparison.OrdinalIgnoreCase ) ) return;
        if ( !Prefab.IsValid() )
        {
            Log.Warning( $"TikFinity spawn action '{Route}' has no valid prefab." );
            return;
        }

        var count = interaction.Amount < 1 ? 1 : interaction.Amount;
        var maximum = MaxSpawnsPerInteraction < 1 ? 1 : MaxSpawnsPerInteraction;
        if ( count > maximum ) count = maximum;

        var origin = SpawnPoint.IsValid() ? SpawnPoint.WorldPosition : WorldPosition;
        var rotation = SpawnPoint.IsValid() ? SpawnPoint.WorldRotation : WorldRotation;

        for ( var i = 0; i < count; i++ )
        {
            var position = origin + RandomOffset();
            var instance = Prefab.Clone( position, rotation );

            if ( NetworkSpawnedObjects && Networking.IsActive )
            {
                if ( !instance.NetworkSpawn( Connection.Host ) )
                {
                    instance.Destroy();
                    continue;
                }
            }

            if ( LifetimeSeconds > 0f ) _ = DestroyLater( instance );
        }
    }

    Vector3 RandomOffset()
    {
        if ( RandomRadius <= 0f ) return Vector3.Zero;
        var angle = Game.Random.NextSingle() * 360f;
        var distance = Game.Random.NextSingle() * RandomRadius;
        return Rotation.FromYaw( angle ).Forward * distance;
    }

    async Task DestroyLater( GameObject instance )
    {
        await Task.DelaySeconds( LifetimeSeconds );
        if ( Networking.IsHost && instance.IsValid() ) instance.Destroy();
    }
}