Enemy subclass representing a destructible barrel object. It sets barrel-specific stats and behavior, updates position with wind/friction, handles flinch visuals, spawns loot and gibs on death, and plays hurt/death sound effects.
using Sandbox;
using Sandbox.UI;
using System;
using System.Threading;
public class Barrel : Enemy
{
public override EnemyType EnemyType => EnemyType.Barrel;
public override float GetMaxHealth()
{
return 45f;
}
public override bool CanHaveTarget => false;
public override bool CanAttack => false;
public override bool CanTurn => false;
public override bool CanBeBackstabbed => false;
public override bool CountsAsKill => false;
public override bool CanMove => false;
public override bool IsInanimate => true;
public override bool CanBeTargeted => false;
protected string _debrisName;
public override string GibFolder => "wood";
public override float OverrideGibChance => 1f;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 0;
CoinValueMax = 0;
CoinChance = 0f;
PushStrength = 10000f;
Weight = 2f;
_debrisName = "barrel_debris";
if ( IsProxy )
return;
Deceleration = 2.5f;
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{Health}", new global::Transform( WorldPosition ) );
if ( IsProxy || IsDying || IsSpawning )
return;
if ( Manager.Instance.IsWindActive )
Velocity += (Manager.Instance.GlobalWindForce / (Weight * 3f)) * Time.Delta;
Velocity *= Math.Max( 1f - Time.Delta * Deceleration * Manager.Instance.GlobalFrictionModifier, 0f );
WorldPosition += (Vector3)Velocity * Time.Delta;
}
public override void Flinch( float time, Vector2 dir )
{
base.Flinch( time, dir );
ModelRenderer.LocalRotation = new Angles( Game.Random.Float( -10f, 10f ), 0f, Game.Random.Float( -10f, 10f ) );
//Transform.ClearInterpolation();
}
public override void StopFlinching()
{
base.StopFlinching();
ModelRenderer.LocalRotation = new Angles( 0f, 0f, 0f );
}
protected override void HandleAnimation()
{
}
public override void SetAnim( string name, bool forceRestart = false )
{
}
protected override void DropLoot( Player player )
{
var dropDir = player.IsValid() ? (player.Position2D - Position2D).Normal : Utils.GetRandomVector();
float lootChanceBonus = player.IsValid() ? player.GetSyncStat( PlayerStat.BarrelLootChance ) : 0f;
// COINS
int numCoins = Game.Random.Int( 2, 3 );
for ( int i = 0; i < numCoins; i++ )
{
var pos = Position2D + Utils.GetRandomVectorInCone( dropDir, coneDegrees: 180f ) * Game.Random.Float( 1f, 3f );
var dir = (pos - Position2D).Normal;
var magnetizeChance = player.IsValid() ? player.GetSyncStat( PlayerStat.KillMagnetizeCoinChance ) : 0f;
Player magnetizePlayer = Game.Random.Float( 0f, 1f ) < magnetizeChance ? player : null;
Manager.Instance.SpawnCoin( pos, value: Game.Random.Int( 1, 2 ), dir, magnetizePlayer );
}
// HEALTH PACK
float lowestHpPercent = 1f;
foreach ( var p in Manager.Instance.AlivePlayers )
lowestHpPercent = MathF.Min( lowestHpPercent, p.Health / p.GetSyncStat( PlayerStat.MaxHp ) );
var healthPackChance = Utils.Map( lowestHpPercent, 1f, 0f, 0.2f, 0.75f );
healthPackChance *= Utils.Select( Manager.Instance.Difficulty, 1.25f, 1f, 1f );
//Log.Info( $"Barrel DropLoot - health_pack: {healthPackChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < healthPackChance * (1f + lootChanceBonus) )
Manager.Instance.SpawnItemRpc( "health_pack", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
// MAGNET
if ( Manager.Instance.TimeSinceMagnet > 50f )
{
var magnetChance = 0.05f * Utils.Map( Manager.Instance.TimeSinceMagnet, 50f, 480f, 1f, 6.5f, EasingType.Linear );
//Log.Info( $"Barrel DropLoot - magnet: {magnetChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < magnetChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "magnet", pos, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
}
}
// ARMOR
var armorChance = Utils.Select( Manager.Instance.Difficulty, 0.33f, 0.15f, 0.1f );
//Log.Info( $"Barrel DropLoot - armor: {armorChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < armorChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "armor_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
}
var bombChance = 0.15f;
//Log.Info( $"Barrel DropLoot - bomb: {bombChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < bombChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "bomb", Position2D, Utils.GetRandomVector() ); // todo: player that destroyed barrel should be Player that created bomb?
}
var rerollChance = Utils.Select( Manager.Instance.Difficulty, 0.25f, 0.2f, 0.2f );
//Log.Info( $"Barrel DropLoot - reroll (x2): {rerollChance * (1f + lootChanceBonus) * 100f:F1}%" );
for ( int i = 0; i < 2; i++ )
{
if ( Game.Random.Float( 0f, 1f ) < rerollChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "reroll_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
}
}
var banishChance = 0.04f;
//Log.Info( $"Barrel DropLoot - banish: {banishChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < banishChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "banish_item", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 160f ) );
}
var numDeadPlayers = Manager.Instance.Players.Count - Manager.Instance.AlivePlayers.Count;
if ( numDeadPlayers > 0 )
{
var reviveChance = Utils.Map( numDeadPlayers, 1, Manager.MAX_PLAYERS - 1, 0.6f, 1f );
//Log.Info( $"Barrel DropLoot - revive: {reviveChance * (1f + lootChanceBonus) * 100f:F1}%" );
if ( Game.Random.Float( 0f, 1f ) < reviveChance * (1f + lootChanceBonus) )
{
var pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 1f, 5f );
Manager.Instance.SpawnItem( "revive_soul", Position2D, Utils.GetRandomVectorInCone( dropDir, coneDegrees: 180f ) );
}
}
CheckHazard();
}
protected void CheckHazard()
{
bool isHazard = false;
Player targetPlayer = null;
foreach ( var player in Manager.Instance.Players )
{
if ( !player.IsValid() )
continue;
var hazardChange = player.GetSyncStat( PlayerStat.BarrelHazardChance );
if( Game.Random.Float( 0f, 1f ) < hazardChange )
{
isHazard = true;
targetPlayer = player;
break;
}
}
if( isHazard && targetPlayer.IsValid() )
ChestEvil.SpawnHazard( targetPlayer.Position2D, chestPos: Position2D, enemySource: this, enemyType: this.EnemyType );
}
protected override void SpawnGibs( Vector2 dir, float force, DamageType damageType )
{
//GameObject.Clone( $"prefabs/effects/{_debrisName}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );
GameObject.Clone( $"prefabs/effects/dark_cloud_explosion.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( WorldPosition.WithZ( Game.Random.Float( 40f, 60f ) ), Rotation.Identity ) } );
SpawnGibs( "fragment", Game.Random.Int( 4, 6 ), force, damageType );
SpawnGibs( "fragment_2", Game.Random.Int( 3, 6 ), force, damageType );
SpawnGibs( "fragment_3", Game.Random.Int( 3, 6 ), force, damageType );
}
void SpawnGibs( string name, int count, float force, DamageType damageType )
{
bool isExplodingBarrel = this is BarrelExploding;
for ( int i = 0; i < count; i++ )
{
var color = Color.Lerp( TintFullHp, TintZeroHp, Game.Random.Float( 0.25f, 1f ) );
if ( isExplodingBarrel )
color = Color.Lerp( color, new Color( 0.4f, 0f, 0f ), 0.8f );
SpawnGoreGib(
$"{GibFolder}/{name}",
localPos: new Vector3( 0f, 0, Game.Random.Float( 20f, 45f ) ) + Vector3.Random * Game.Random.Float( 0f, 20f ),
localRot: Rotation.Random,
scaleMultiplier: Game.Random.Float( 1f, 1.5f ),
dir: Vector3.Random,
force: force * Game.Random.Float( 0.05f, 0.8f ),
color,
damageType
);
}
}
public override void PlayHurtSfx( float damage, DamageType damageType, Vector3 hitPos, Player player, DamageResultFlags damageFlags )
{
// sfx
if ( _realTimeSinceHurtSfx > 0.0175f )
{
if ( damage == 0f )
{
Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Game.Random.Float( 1.5f, 1.6f ), volume: 1.3f, maxDist: 300f );
}
else if ( damageType == DamageType.Punch ) { Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1.2f, 1.4f, EasingType.SineIn ), volume: 1.3f, maxDist: 400f ); }
//else if ( damageType == DamageType.DashSlash ) { Manager.Instance.PlaySfxNearby( "player.dash.slash.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 0.9f, 1.1f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 0.8f, maxDist: 350f ); }
else if ( damageType == DamageType.Fire ) { Manager.Instance.PlaySfxNearby( "burn_2", hitPos, pitch: Game.Random.Float( 1.15f, 1.35f ), volume: 0.7f, maxDist: 300f ); }
else if ( damageType == DamageType.Poison ) { Manager.Instance.PlaySfxNearby( "poisoned", hitPos, pitch: Game.Random.Float( 1.55f, 1.65f ), volume: 0.35f, maxDist: 300f ); }
else if ( damageType == DamageType.SpikerHead ) { Manager.Instance.PlaySfxNearby( "spike.stab", hitPos, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.8f, maxDist: 300f ); }
else if ( damageType == DamageType.SpitterProjectile || damageType == DamageType.SpitterProjectileHoming ) { Manager.Instance.PlaySfxNearby( "splash", hitPos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f ); }
else if ( damageType == DamageType.Aoe || damageType == DamageType.BulletSplash ) { /* no sfx */ }
else if ( damageType == DamageType.Radiation ) { /* no sfx */ }
else if ( damageType == DamageType.Shock ) { /* no sfx */ }
else if ( damageType == DamageType.Explosion ) { /* no sfx */ }
else if ( damageType == DamageType.JumpFinish ) { Manager.Instance.PlaySfxNearby( "slam", hitPos, pitch: Game.Random.Float( 0.85f, 0.95f ), volume: 0.8f, maxDist: 150f ); }
else { Manager.Instance.PlaySfxNearby( "barrel.hit", hitPos, pitch: Utils.Map( Health, MaxHealth, 0f, 1f, 1.25f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.05f ), volume: 1.3f, maxDist: 400f ); }
// todo: OrbitingBlade hit sfx
_realTimeSinceHurtSfx = 0f;
}
}
protected override void PlayDeathSfx( Vector2 pos )
{
Manager.Instance.PlaySfxNearby( "barrel.die", pos, pitch: Game.Random.Float( 0.9f, 1.3f ), volume: 1.8f, maxDist: 500f );
}
}