perks/PerkBulletGroundHop.cs

A unique perk called Chaotic Echo that, when a bullet hits the ground, spawns a new bullet copy with half damage and inherits some stats. It plays a nearby sfx, spawns a visual ring, sets spawn-from-ground flags, increments a ground-hop counter, and preserves arc behavior and velocity.

Networking
using System;
using Sandbox;

[Perk( Rarity.Unique, locked: true, alwaysOfferDebug: false )]
public class PerkBulletGroundHop : Perk
{
	private enum Mod { EchoLimit };

	static PerkBulletGroundHop()
	{
		Register<PerkBulletGroundHop>(
			name: "Chaotic Echo",
			imagePath: "textures/icons/vector/bullet_ground_hop.png",
			description: level => $"When bullet-icon hit the ground\nthey spawn a new copy\nthat does 50% dmg"
		);
	}

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

		HighlightColor = new Color( 0.6f, 0.6f, 0.6f );
		HighlightDuration = 0.1f;
		HighlightOpacity = 2f;
	}

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

	}

	public override void OnBulletHitGround( Bullet bullet )
	{
		base.OnBulletHitGround( bullet );

		int numHopsDone = (int)bullet.Stats[BulletStat.NumGroundHops];

		if ( numHopsDone >= (int)GetValue(Level, Mod.EchoLimit) )
			return;

		// todo: better sfx
		Manager.Instance.PlaySfxNearbyRpc( "bullet.impact", bullet.Position2D, pitch: Game.Random.Float( 1.6f, 1.7f ), volume: 0.75f, maxDist: 500f );

		// todo: better texture
		Manager.Instance.SpawnRingRpc( bullet.Position2D, Game.Random.Float(12f, 16f), new Color( 1f, 1f, 0.2f, 0.75f ), lifetime: Game.Random.Float( 0.15f, 0.2f ), path: "ring_explosion_2" );

		var dmg = bullet.Stats[BulletStat.Damage] * 0.5f;
		var dir = Utils.GetRandomVector();
		var b = Player.SpawnBullet( bullet.Position2D, dir, dmg, isFromClip: false, bulletType: bullet.BulletType );
		b.Stats[BulletStat.StartFromGround] = 1f;
		b.WorldPosition = b.WorldPosition.WithZ( 0f );
		b.Stats[BulletStat.NumGroundHops] = numHopsDone + 1;
		b.Velocity = dir * bullet.Velocity.Length;

		if ( bullet.Stats[BulletStat.ArcHeight] > 0f )
			b.SetupArc( bullet.Stats[BulletStat.ArcHeight], Player.Stats[PlayerStat.ArcBulletBounces] );

		//Highlight();
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.EchoLimit:
			default:
				return level;
		}
	}
}