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