Bomb.cs
using System;
using System.Diagnostics;
using System.Linq;
using Sandbox;
using Sandbox.Diagnostics;
namespace Facepunch.BombRoyale;
[Title( "Bomb" )]
[Category( "Bomb Royale" )]
public class Bomb : Component, IRestartable
{
[Sync] private Guid PlacerId { get; set; }
public Player Player => Scene.Directory.FindComponentByGuid( PlacerId ) as Player;
[Sync] public bool IsPlaced { get; private set; }
[Sync] private TimeSince TimeSincePlaced { get; set; }
[Sync] public int Range { get; private set; }
[Property] public ModelRenderer Renderer { get; set; }
private TimeUntil NextBlinkTime { get; set; }
private TimeUntil BlinkEndTime { get; set; }
private float LifeTime { get; set; } = 4f;
private SoundHandle FuseSound { get; set; }
private bool HasExploded { get; set; }
private GameObject WickEffect { get; set; }
protected override void OnAwake()
{
UpdateTags();
base.OnAwake();
}
void IRestartable.OnRestart()
{
GameObject.Destroy();
}
public void Place( Player player )
{
Assert.True( Networking.IsHost );
TimeSincePlaced = 0f;
var gridPosition = player.WorldPosition.SnapToGrid( 32f );
if ( player.HasSuperBomb )
{
player.HasSuperBomb = false;
Range = 10;
}
else if ( player.Disease == DiseaseType.LowRange )
{
Range = 1;
}
else
{
Range = player.BombRange;
}
GameObject.Parent = null;
WorldPosition = new( gridPosition.x, gridPosition.y, player.WorldPosition.z );
WorldScale = 1f;
IsPlaced = true;
PlacerId = player.Id;
StartFuseSound( WorldPosition );
SpawnWickEffect();
}
public void Pickup( Player player )
{
Assert.True( Networking.IsHost );
player.SetHoldingBomb( this );
GameObject.SetParent( player.GameObject );
WorldPosition = player.WorldPosition + Vector3.Up * 80f + player.WorldRotation.Forward * 4f;
IsPlaced = false;
StopFuseSound();
DestroyWickEffect();
}
private void UpdateTags()
{
Tags.Add( "solid" );
Tags.Add( "bomb" );
Tags.Set( "bomb_placed", IsPlaced );
Tags.Set( "passable", IsAnyPlayerColliding() );
}
[Rpc.Broadcast]
private void StartFuseSound( Vector3 position )
{
FuseSound?.Stop();
FuseSound = Sound.Play( "bomb.fuse", position );
}
private void StopFuseSound()
{
FuseSound?.Stop();
FuseSound = null;
}
[Rpc.Broadcast]
private void SpawnWickEffect()
{
var renderer = Components.Get<SkinnedModelRenderer>( FindMode.EnabledInSelfAndChildren );
var bonePosition = Vector3.Zero;
if ( renderer.TryGetBoneTransform( "wick", out var boneTx ) )
{
bonePosition = boneTx.Position;
}
WickEffect = GameObject.Clone( "prefabs/effects/bomb_wick.prefab", new CloneConfig
{
StartEnabled = true,
Transform = new Transform( bonePosition ),
Parent = GameObject,
Name = "WickEffect"
} );
}
private void DestroyWickEffect()
{
if ( !WickEffect.IsValid() )
return;
WickEffect.Destroy();
WickEffect = null;
}
protected override void OnFixedUpdate()
{
UpdateTags();
Tick();
base.OnFixedUpdate();
}
private bool IsAnyPlayerColliding()
{
var players = Scene.GetAllComponents<Player>();
foreach ( var player in players )
{
if ( player.LifeState != LifeState.Alive )
continue;
if ( player.IsInsideBomb( this ) )
return true;
}
return false;
}
private void Tick()
{
if ( !IsPlaced || BombRoyale.IsPaused ) return;
if ( !Networking.IsHost ) return;
if ( TimeSincePlaced < LifeTime ) return;
Explode();
}
protected override void OnPreRender()
{
UpdateSceneObject();
base.OnPreRender();
}
private void UpdateSceneObject()
{
var sceneObject = Renderer.SceneObject;
if ( !sceneObject.IsValid() || !Player.IsValid() )
return;
var tx = sceneObject.Transform;
if ( IsPlaced )
{
tx.Scale = 1f + (MathF.Sin( Time.Now * 10f ) * 0.15f);
if ( Player.IsValid() )
{
sceneObject.Attributes.Set( "BombColor", Player.GetTeamColor() );
}
if ( NextBlinkTime )
{
sceneObject.Attributes.Set( "ExplodeTime", 1f );
if ( BlinkEndTime )
NextBlinkTime = 1f * (1f - (TimeSincePlaced / LifeTime)).Clamp( 0.1f, 1f );
}
else
{
sceneObject.Attributes.Set( "ExplodeTime", 0f );
BlinkEndTime = 0.1f;
}
}
else
{
tx.Scale = 1f;
}
sceneObject.Transform = tx;
}
private void ShortenFuse( float time )
{
if ( TimeSincePlaced < LifeTime - time )
{
TimeSincePlaced = LifeTime - time;
}
}
[Rpc.Broadcast]
private void DoScreenShake()
{
var shake = new ScreenShake.Random( 1.5f, 1f + (Range * 0.5f) );
ScreenShake.Add( shake );
}
[Rpc.Broadcast]
private void PlayExplodeSound( Vector3 position )
{
Sound.Play( "bomb.explode", position );
}
private void Explode()
{
Assert.True( Networking.IsHost );
if ( HasExploded ) return;
DoScreenShake();
DestroyWickEffect();
HasExploded = true;
BlastInDirection( Vector3.Forward );
BlastInDirection( Vector3.Backward );
BlastInDirection( Vector3.Left );
BlastInDirection( Vector3.Right );
PlayExplodeSound( WorldPosition );
if ( Game.Random.Float() < 0.5f )
{
var availableBlock = Scene.GetAllComponents<Bombable>()
.Where( e => !e.IsSpaceOccupied() )
.Shuffle()
.FirstOrDefault();
if ( availableBlock.IsValid() )
{
Facepunch.BombRoyale.Pickup.CreateRandom( availableBlock.Renderer.Bounds.Center );
}
}
StopFuseSound();
GameObject.Destroy();
}
[Rpc.Broadcast]
private void CreateBombParticles( Vector3 startPosition, Vector3 endPosition )
{
BombExplosionEffect.Create( Scene, startPosition, endPosition );
}
private void BlastInDirection( Vector3 direction )
{
var startPosition = WorldPosition + Vector3.Up * 16f;
var cellSize = 32f;
var totalRange = (Range * cellSize);
var trace = Scene.Trace.Ray( startPosition, startPosition + direction * totalRange )
.Radius( 8f )
.WithAnyTags( "solid", "player", "pickup", "bomb_placed" )
.WithoutTags( "destroyed", "spreader" )
.HitTriggers()
.IgnoreGameObject( GameObject )
.Run();
CreateBombParticles( trace.StartPosition, trace.EndPosition + trace.Direction * (cellSize * 0.5f) );
var hitObject = trace.GameObject;
if ( !hitObject.IsValid() ) return;
if ( hitObject.Components.TryGet<Bombable>( out var bombable, FindMode.EverythingInSelfAndAncestors ) )
{
if ( Player.IsValid() )
{
Player.IncrementStat( "blocks_exploded" );
}
bombable.Break();
bombable.TrySpawnPickup();
bombable.Hide();
}
else if ( hitObject.Components.TryGet<Player>( out var player, FindMode.EverythingInSelfAndAncestors ) )
{
player.TakeDamage( DamageType.Explosion, 0f, trace.EndPosition, Vector3.Zero, Player );
}
else if ( hitObject.Components.TryGet<Pickup>( out var pickup, FindMode.EverythingInSelfAndAncestors ) )
{
pickup.GameObject.Destroy();
}
else if ( hitObject.Components.TryGet<Bomb>( out var bomb, FindMode.EverythingInSelfAndAncestors ) )
{
var fuseDelay = Game.Random.Float( 0.15f, 0.3f );
bomb.ShortenFuse( fuseDelay );
}
}
}