things/orbiters/OrbiterBlade.cs

OrbiterBlade is a game entity that orbits a unit and deals melee-style damage to enemies on contact. It manages rotation, visibility, hit timing, damage scaling with perks, and can spawn bullets on hit.

NetworkingReflection
using System;
using Sandbox;

public class OrbiterBlade : Orbiter
{
	[Sync] public float DamagePercent { get; set; }

	private TimeSince _timeSinceDamage;
	private float _lastAlpha;

	public int BladeNum { get; set; }

	private Dictionary<Thing, float> _damageTimes;
	private Enemy _lastEnemyHit;
	private int _consecutiveHitsOnSameEnemy;

	public float RotationSpeed { get; set; }

	public float OriginalOrbitSpeed { get; set; }

	public float Damage { get; set; }

	public Player Player { get; set; }

	protected override void OnStart()
	{
		base.OnStart();

		if ( IsProxy )
			return;

		CollideWithTags.Add( "enemy" );

		OrbitDistance = 40f;
		_lastAlpha = -1f;

		_damageTimes = new();
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		float alpha = OrbitedUnit.IsValid() && !OrbitedUnit.IsDying
			? 1f
			: 0f;

		if( alpha != _lastAlpha)
		{
			Model.Tint = Model.Tint.WithAlpha( alpha );
			_lastAlpha = alpha;
		}

		if ( IsProxy )
			return;

		if ( !OrbitedUnit.IsValid() || OrbitedUnit.IsDying )
			return;

		//WorldRotation = Rotation.LookAt( Position2D - OrbitedUnit.Position2D );

		// todo: speed up rotation speed with PerkOrbiterBladeHitInterval
		WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Up, RotationSpeed * Time.Delta );

		var hitStopTime = Player.IsValid() && !Player.IsProxy ? Player.Stats[PlayerStat.OrbiterBladeStopTime] : 0f;
		var orbitSpeedMult = Player.IsValid() && !Player.IsProxy ? Player.Stats[PlayerStat.OrbiterBladeSpeedMultiplier] : 1f;
		var targetOrbitSpeed = OriginalOrbitSpeed * orbitSpeedMult;

		OrbitSpeed = _timeSinceDamage < hitStopTime ? 0f : targetOrbitSpeed;

		// todo: orbit faster while standing still ?

		OrbitDistance = OrbitedUnit.Radius + 55f + BladeNum * 22f;

		if ( _timeSinceDamage < 0.25f )
		{
			WorldScale = new Vector3( Utils.Map( _timeSinceDamage, 0f, 0.25f, 0.5f, 1f, EasingType.SineOut ) );
		}

		var hitInterval = Player.IsValid() && !Player.IsProxy ? Player.Stats[PlayerStat.OrbiterBladeHitInterval] : 1f;
		// todo: change rotation speed based on hit interval

		for ( int i = _damageTimes.Count - 1; i >= 0; i-- )
		{
			var pair = _damageTimes.ElementAt( i );
			if ( Time.Now > pair.Value + hitInterval )
				_damageTimes.Remove( pair.Key );
		}
	}

	protected override float GetZPos()
	{
		return OrbitedUnit.WorldPosition.z + 35f;
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( !OrbitedUnit.IsValid() || OrbitedUnit.IsDying || _damageTimes.ContainsKey( other ) || Manager.Instance.IsGameOver )
			return;

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			if ( !_lastEnemyHit.IsValid() )
			{
				_lastEnemyHit = null;
				_consecutiveHitsOnSameEnemy = 0;
			}

			if ( _lastEnemyHit == enemy )
				_consecutiveHitsOnSameEnemy++;
			else
			{
				_lastEnemyHit = enemy;
				_consecutiveHitsOnSameEnemy = 1;
			}

			WorldScale = new Vector3( 0.5f );
			_timeSinceDamage = 0f;

			var dir = Utils.GetPerpendicularVector( (Position2D - OrbitedUnit.Position2D).Normal ) * (OrbitSpeed < 0f ? 1f : -1f);
			var player = OrbitedUnit as Player;

			if ( player.IsValid() )
			{
				var finalDamage = Damage;
				var sameEnemyExtraHits = Math.Max( 0, _consecutiveHitsOnSameEnemy - 1 );
				var perkLevel = 0;
				var perkType = TypeLibrary.GetType( typeof( PerkOrbiterBladeConsecutiveDmg ) );
				if ( perkType != null )
					perkLevel = player.GetPerkLevel( perkType );

				if ( perkLevel > 0 && sameEnemyExtraHits > 0 )
				{
					var flatBonusPerHit = PerkOrbiterBladeConsecutiveDmg.GetFlatBonusPerConsecutiveHit( perkLevel );
					var maxFlatBonus = PerkOrbiterBladeConsecutiveDmg.GetMaxFlatBonus( perkLevel );
					var flatBonus = Math.Min( maxFlatBonus, flatBonusPerHit * sameEnemyExtraHits );
					finalDamage += flatBonus;
				}

				enemy.DamageRpc( finalDamage, player, DamageType.OrbitingBlade, WorldPosition, dir * finalDamage * 20f, isCrit: false, shouldFlinch: true );

				var bulletChance = player.Stats[PlayerStat.OrbiterBladeBulletChance];
				if( Game.Random.Float(0f, 1f) < bulletChance )
				{
					var bulletDir = (Position2D - enemy.Position2D).Normal;
					var dmg = player.GetBulletDamage( isFromClip: false, isLastAmmo: false );
					var bulletPos = Position2D + bulletDir * enemy.Radius * 1.5f;
					var b = Player.SpawnBullet( bulletPos, Utils.RotateVector( bulletDir, Game.Random.Float( -20f, 20f ) ), dmg, isFromClip: false, bulletType: BulletType.Normal );

					// todo: bone model for bullet
					// todo: Highlight perk

					if ( other.IsValid() )
						b.HitThings.Add( other );

					// todo: sfx
					//Manager.Instance.PlaySfxNearbyRpc( "player.shoot", bulletPos, pitch: Game.Random.Float( 1.2f, 1.3f ), volume: 1f, maxDist: 350f );
				}
			}

			_damageTimes.Add( other, Time.Now );
		}
	}
}