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 );
		}
	}
}