perks/PerkFearGaze.cs

A legendary perk component that implements a "Fear Gaze" ability for a player. It spawns particle effects, traces a forward cone to detect enemy units, accumulates gaze time on a targeted unit and applies a fear status when the gaze duration exceeds a threshold; it also updates particle visuals while gazing.

File Access
using System;
using Sandbox;

[Perk( Rarity.Legendary, disabled: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Fear })]
public class PerkFearGaze : Perk
{
	private enum Mod { Time };

	private float _tickTimer;
	private const float TICK_TIME = 0.15f;

	private Unit _targetUnit;
	private float _gazeTime;

	private GameObject _particles;
	private ParticleEffect _particleRing;
	private ParticleRingEmitter _particleRingEmitter;

	private const float GAZE_RANGE = 350f;


	static PerkFearGaze()
	{
		//Register<PerkFearGaze>(
		//	name: "Fearsome Gaze",
		//	imagePath: "textures/icons/vector/fear_gaze.png",
		//	description: level => $"Staring at enemies for {GetValue( level, Mod.Time ).ToString( "0.#" )}s\nscares them",
		//	upgradeDescription: level => $"Staring at enemies for {GetValue( level - 1, Mod.Time ).ToString("0.#")}→{GetValue( level, Mod.Time ).ToString("0.#")}s\nscares them"
		//);
	}

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

		HighlightColor = new Color( 0.5f, 0f, 1f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 3f;

		_particles = GameObject.Clone( "prefabs/effects/fear_gaze_particles.prefab", new global::Transform( Player.WorldPosition.WithZ( -100f ) ) );
		_particleRing = _particles.GetComponent<ParticleEffect>();
		_particleRing.Alpha = 0f;
		_particleRingEmitter = _particles.GetComponent<ParticleRingEmitter>();
		_particleRingEmitter.Rate = 0;

		ShouldUpdate = true;
	}

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

	}

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

		// todo: hide the particle effect ring when player dead

		//Gizmo.Draw.Color = Color.Blue;
		//Gizmo.Draw.LineSphere( Player.WorldPosition.WithZ( 50f ), GAZE_RANGE );

		_tickTimer += dt;
		if ( _tickTimer > TICK_TIME )
		{
			Vector3 a = new Vector3( Player.Position2D.x, Player.Position2D.y, 32f ) + Player.Model.LocalRotation.Forward * 1f;
			Vector3 b = a + Player.Model.LocalRotation.Forward * GAZE_RANGE;

			bool hitSomething = false;

			var traceResults = Player.Scene.Trace.Sphere( 10f, a, b ).WithAnyTags( "enemy", "player" ).HitTriggersOnly().RunAll().ToList();
			foreach ( var tr in traceResults )
			{
				var unit = tr.GameObject.GetComponent<Unit>();

				if ( unit.IsValid() && !unit.IsDying && !unit.IsInanimate && !unit.IsFearful )
				{
					if ( unit is Player )
						continue;

					if ( _targetUnit != unit )
					{
						_targetUnit = unit;
						_gazeTime = 0f;
						_particleRing.Alpha = 0f;
						_particleRingEmitter.Radius = _targetUnit.Radius * 1.4f;
					}

					_gazeTime += TICK_TIME;
					if ( _gazeTime > GetValue( Level, Mod.Time ) )
					{
						unit.Fear( Player, enemySource: null, lifetime: Player.Stats[PlayerStat.FearLifetime] );

						_gazeTime = 0f;

						Highlight();
					}

					hitSomething = true;

					break;
				}
			}

			if ( !hitSomething )
			{
				_gazeTime = MathX.Lerp( _gazeTime, 0f, 0.3f * dt );
				_targetUnit = null;
			}

			_tickTimer -= TICK_TIME;
		}

		if ( _targetUnit.IsValid() && !_targetUnit.IsFearful )
		{
			//Gizmo.Draw.Color = Color.Red;
			//Gizmo.Draw.LineSphere( _targetUnit.WorldPosition.WithZ( 50f ), 80f * Utils.Map( _gazeTime, 0f, GetValue( Level, Mod.Time ), 1f, 0f ) );

			_particles.WorldPosition = _targetUnit.WorldPosition.WithZ( 2f );
			_particleRing.Alpha = Utils.Map( _gazeTime, 0f, GetValue( Level, Mod.Time ), 0f, 1f, EasingType.SineIn );
			_particleRingEmitter.Radius = Utils.Map( _gazeTime, 0f, GetValue( Level, Mod.Time ), _targetUnit.Radius * 1.4f, _targetUnit.Radius * 0.25f, EasingType.SineIn );
			_particleRingEmitter.Rate = 500;
		}
		else
		{
			_particles.WorldPosition = _particles.WorldPosition.WithZ( -100f );
			_particleRing.Alpha = 0f;
			_particleRingEmitter.Rate = 0;
		}
	}

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

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _particles != null )
			_particles.Destroy();
	}
}