perks/PerkArtillery.cs

A Mythic perk called Artillery. It periodically picks a random position near the player (clamped to game bounds), spawns an artillery shell prefab there, assigns the player as the shooter, network-spawns the object for the player's owner, resets its internal cooldown, and tells the Manager to show a warning at the impact position.

NetworkingFile AccessNative Interop
using System;
using Sandbox;

[Perk( Rarity.Mythic, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Artillery, PerkCategory.Aoe, PerkCategory.Explosion })]
public class PerkArtillery : Perk
{
	private enum Mod { RechargeTime };

	private float _timer;


	public override float ImportanceMultiplier => 1.2f;

	static PerkArtillery()
	{
		Register<PerkArtillery>(
			name: "Artillery",
			imagePath: "textures/icons/vector/artillery.png",
			description: level => $"An explosive missile\nlands nearby every {GetValue( level, Mod.RechargeTime )}s",
			upgradeDescription: level => $"An explosive missile\nlands nearby every {GetValue( level - 1, Mod.RechargeTime )}→{GetValue( level, Mod.RechargeTime )}s"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;
	}

	public override void Refresh()
	{
		base.Refresh();

	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.RechargeTime:
			default:
				return 2.5f - level * 0.5f;
		}
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		_timer += dt;
		if ( _timer > GetValue( Level, Mod.RechargeTime ) )
			Shoot();
	}

	public void Shoot()
	{
		Vector2 pos = Player.Position2D + Utils.GetRandomVector() * Game.Random.Float( 100f, 250f );

		Vector2 min = Manager.Instance.BOUNDS_MIN;
		Vector2 max = Manager.Instance.BOUNDS_MAX;

		if ( pos.x < min.x )
			pos = new Vector2( min.x + (min.x - pos.x), pos.y );
		else if ( pos.x > max.x )
			pos = new Vector2( max.x - (pos.x - max.x), pos.y );

		if ( pos.y < min.y )
			pos = new Vector2( pos.x, min.y + (min.y - pos.y) );
		else if ( pos.y > max.y )
			pos = new Vector2( pos.x, max.y - (pos.y - max.y) );

		var artilleryShellGo = GameObject.Clone( $"prefabs/artillery_shell.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( position: new Vector3( pos.x, pos.y, -100f ), rotation: new Angles( 90f, Game.Random.Float( 0f, 360f ), 0f ) ) } );

		var artilleryShell = artilleryShellGo.GetComponent<ArtilleryShell>();
		artilleryShell.Shooter = Player;

		artilleryShellGo.NetworkSpawn( Player.Network.Owner );

		_timer = 0f;

		Manager.Instance.SpawnArtilleryWarning( pos );
	}
}