An OrbiterBlade entity derived from Orbiter. It orbits a unit, handles rendering alpha/scale based on the orbited unit and recent hits, rotates around the unit, tracks recent damage timestamps to avoid repeated hits, and applies damage and effects to Enemy targets (including perk-based extra damage and chance to spawn a bullet).
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 );
}
}
}