Projectile entity for an enemy spitter. It manages projectile type, lifespan, movement including Z easing and wind, particle effects, spawning acid puddles for acid projectiles, hit behavior and removal effects.
using Sandbox;
using System;
public class SpitterProjectile : EnemyProjectile
{
//[Property] public GameObject Particles { get; set; }
[Property] public GameObject Model { get; set; }
[Property] public GameObject ModelAnchor { get; set; }
[Property] public SkinnedModelRenderer Renderer { get; set; }
[Sync] public float Damage { get; set; }
public float Lifetime { get; set; }
[Sync] public Enemy Shooter { get; set; }
public EnemyType EnemyType { get; set; }
public float BaseZPos { get; set; }
public float HitForce { get; set; }
protected Color _impactParticleColor;
[Property] public EnemyProjectileType ProjectileType { get; protected set; }
protected GameObject _particleObject;
public virtual float RequiredPlayerCollisionPercent => 0.05f;
protected float _groundHeight;
protected EasingType _zPosEasingType;
private TimeSince _timeSinceAcidPuddle;
private float _timeUntilNextAcidPuddle;
private const float POSITION_HISTORY_DELAY = 0.5f;
private Queue<(float time, Vector2 position)> _positionHistory = new();
public Vector2 DelayedPosition { get; private set; }
private bool _hasSetDelayedPosition = false;
public virtual bool ShouldSpin => true;
private float _spinSpeed;
protected float _velocityScale;
protected override void OnStart()
{
base.OnStart();
//Model.LocalRotation = Rotation.Random;
_spinSpeed = Game.Random.Float( 250f, 700f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
CreateParticles();
if ( IsProxy )
return;
_velocityScale = Game.Random.Float( 0.95f, 1.05f );
_groundHeight = 0f;
_zPosEasingType = EasingType.QuadIn;
ShouldCheckBounds = Manager.Instance.EnemyProjectileBounceFenceLevel > 0;
}
public virtual void SetProjectileType( EnemyProjectileType projectileType )
{
ProjectileType = projectileType;
switch ( projectileType )
{
case EnemyProjectileType.Normal:
default:
HitForce = 220f;
Damage = Utils.Select( Manager.Instance.Difficulty, 7f, 8f, 9f );
Lifetime = 6f;
break;
case EnemyProjectileType.Acid:
HitForce = -50f;
Damage = Utils.Select( Manager.Instance.Difficulty, 4f, 5f, 6f );
Lifetime = 4f;
_timeUntilNextAcidPuddle = Game.Random.Float( 0.1f, 0.2f ) * Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 1f );
_timeSinceAcidPuddle = 0f;
break;
case EnemyProjectileType.Fire:
HitForce = 50f;
Damage = Utils.Select( Manager.Instance.Difficulty, 2f, 3f, 4f );
Lifetime = 6.5f;
break;
case EnemyProjectileType.Freeze:
HitForce = 60f;
Damage = Utils.Select( Manager.Instance.Difficulty, 4f, 5f, 6f );
Lifetime = 8f;
break;
case EnemyProjectileType.Poison:
HitForce = 50f;
Damage = Utils.Select( Manager.Instance.Difficulty, 3f, 4f, 5f );
Lifetime = 4.5f;
break;
case EnemyProjectileType.Curse:
HitForce = 30f;
Damage = Utils.Select( Manager.Instance.Difficulty, 3f, 3f, 3f );
Lifetime = 5f;
break;
}
Lifetime *= Utils.Select( Manager.Instance.Difficulty, 0.85f, 1f, 1.2f );
}
protected virtual void CreateParticles()
{
switch ( ProjectileType )
{
case EnemyProjectileType.Normal:
default:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_normal.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 0f );
_impactParticleColor = new Color(0.7f, 0.7f, 0.7f);
break;
case EnemyProjectileType.Acid:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_acid.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 0f );
_impactParticleColor = Color.Yellow;
break;
case EnemyProjectileType.Fire:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_fire.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 15f );
_impactParticleColor = new Color( 1f, 0f, 0.8f );
break;
case EnemyProjectileType.Freeze:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_freeze.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 0f );
_impactParticleColor = new Color( 0.3f, 0.5f, 1f );
break;
case EnemyProjectileType.Poison:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_poison.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 0f );
_impactParticleColor = Color.Green;
break;
case EnemyProjectileType.Curse:
_particleObject = GameObject.Clone( "prefabs/enemyProjectiles/enemy_projectile_particles_curse.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
_particleObject.LocalPosition = new Vector3( 0f, 0f, 0f );
_impactParticleColor = new Color(0.2f, 0f, 0.7f);
break;
}
}
protected override void OnUpdate()
{
base.OnUpdate();
if( ShouldSpin )
ModelAnchor.LocalRotation = new Angles( 0f, 0f, ModelAnchor.LocalRotation.Roll() + _spinSpeed * Time.Delta );
if ( IsProxy )
return;
if( ProjectileType == EnemyProjectileType.Acid )
{
UpdatePositionHistory();
if ( TimeSinceSpawn > 0.4f && TimeSinceSpawn < Lifetime - 0.3f && _timeSinceAcidPuddle > _timeUntilNextAcidPuddle && _hasSetDelayedPosition )
{
_timeSinceAcidPuddle = 0f;
_timeUntilNextAcidPuddle = Game.Random.Float( 0.2f, 0.55f ) * Utils.Select( Manager.Instance.Difficulty, 1.5f, 1.2f, 0.9f );
var acidDmg = Utils.Select( Manager.Instance.Difficulty, 4f, 6f, 9f );
Manager.Instance.SpawnAcidPuddle( DelayedPosition, lifetime: Game.Random.Float( 4f, 5f ), acidDmg, scale: Game.Random.Float( 0.8f, 0.9f ), Color.Yellow, new Color( 0.5f, 0.5f, 0f ), playerSource: null, enemySource: Shooter, enemyType: EnemyType );
}
}
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{TimeSinceSpawn} / {Lifetime}", new global::Transform( WorldPosition ) );
if ( Manager.Instance.IsWindActive )
Velocity += Manager.Instance.GlobalWindForce * Time.Delta;
var zPos = Utils.Map( TimeSinceSpawn, 0f, Lifetime, BaseZPos, 0f, _zPosEasingType );
WorldPosition = (WorldPosition + (Vector3)Velocity * _velocityScale * Time.Delta).WithZ( zPos );
if ( TimeSinceSpawn > Lifetime )
{
RemoveRpc( shouldSpawnEffects: Manager.Instance.IsInBounds( Position2D ) );
}
HandleRotation();
}
protected virtual void HandleRotation()
{
//WorldRotation = WorldRotation.RotateAroundAxis( Vector3.Forward, 500f * Time.Delta );
}
private void UpdatePositionHistory()
{
float currentTime = Time.Now;
_positionHistory.Enqueue( (currentTime, Position2D) );
float targetTime = currentTime - POSITION_HISTORY_DELAY;
while ( _positionHistory.Count > 1 && _positionHistory.Peek().time < targetTime )
{
DelayedPosition = _positionHistory.Dequeue().position;
_hasSetDelayedPosition = true;
}
}
[Rpc.Broadcast]
public void PlayerHitRpc( bool shouldSpawnEffects, bool multiHit, Vector2 playerPos )
{
if ( IsProxy )
return;
if ( multiHit )
Velocity = (Position2D - playerPos).Normal * Math.Max( Velocity.Length, 200f );
else
RemoveRpc( shouldSpawnEffects );
}
protected override void Remove( bool shouldSpawnEffects )
{
base.Remove( shouldSpawnEffects );
Manager.Instance.SpawnBulletImpactParticles( WorldPosition.WithZ( Math.Max( WorldPosition.z, 10f ) ), Vector3.Up, _impactParticleColor );
// todo: should this be done on client or just owner?
if( _particleObject.IsValid() )
{
_particleObject.SetParent( null );
var emitter = _particleObject.GetComponent<ParticleSphereEmitter>();
emitter.DestroyOnEnd = true;
emitter.Loop = false;
}
if ( shouldSpawnEffects )
{
switch ( ProjectileType )
{
case EnemyProjectileType.Normal:
default:
break;
case EnemyProjectileType.Acid:
// todo: different sfx, not the same as player getting hurt by acid
Manager.Instance.PlaySfxNearbyRpc( "puddle_splat", Position2D, pitch: Game.Random.Float( 1.05f, 1.1f ), volume: 1.2f, maxDist: 350f );
break;
case EnemyProjectileType.Fire:
break;
case EnemyProjectileType.Freeze:
break;
}
}
if ( IsProxy )
return;
if ( shouldSpawnEffects )
{
switch ( ProjectileType )
{
case EnemyProjectileType.Normal:
default:
break;
case EnemyProjectileType.Acid:
// todo: don't spawn acid puddle on top of existing acid puddles
var acidDmg = Utils.Select( Manager.Instance.Difficulty, 4f, 6f, 9f );
Manager.Instance.SpawnAcidPuddle( Position2D, lifetime: Game.Random.Float( 8f, 10f ), acidDmg, scale: Game.Random.Float( 1.1f, 1.3f ), Color.Yellow, new Color( 0.5f, 0.5f, 0f ), playerSource: null, enemySource: Shooter, enemyType: EnemyType );
break;
case EnemyProjectileType.Fire:
var fireDmg = Utils.Select( Manager.Instance.Difficulty, 3f, 8f, 10f );
Manager.Instance.SpawnFireGroundRpc( Position2D, player: null, enemySource: Shooter, enemyType: EnemyType, fireDmg, lifetime: Game.Random.Float( 10f, 12f ), spreadChance: 0f, canStack: false, scale: 1f, colorA: Color.Magenta, colorB: Color.Red, hurtPlayers: true, hurtEnemies: false );
break;
case EnemyProjectileType.Freeze:
break;
}
}
}
protected override void OnOutOfBounds( Direction direction )
{
base.OnOutOfBounds( direction );
SetDirection( Velocity.Normal );
TimeSinceSpawn = 0f;
BaseZPos = MathX.Lerp( WorldPosition.z, BaseZPos, 0.5f );
// todo: sfx
}
}