MP5.cs
using System;
using System.Diagnostics;
using Sandbox;

public sealed class MP5 : Component, IPlayerReplicatorEvents, IPlayerHealthEvent
{
	[Property]
	List<SkinnedModelRenderer> Viewmodels;
	[Property]
	List<ModelRenderer> Worldmodels;
	[Property]
	List<SkinnedModelRenderer> Playermodels;
	[Property]
	List<CameraComponent> Cameras;

	[Property]
	public float FireDelay { get; set; } = 0.1f;

	[Property]
	public GameObject MuzzleFlare { get; set; }

	[Property]
	public SoundEvent FireSound { get; set; }

	[Property]
	public int Damage { get; set; } = 6;

	[Property]
	public Texture Crosshair { get; set; }

	[Property]
	public GameObject TempImpact { get; set; }

	TimeSince lastFired;

	protected override void OnEnabled()
	{
		base.OnEnabled();

		lastFired = 0;
		FindViewmodels();
		FindWorldmodels();
		FindPlayermodels();
		FindCameras();
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		var isAttacking = false;

		if ( lastFired > FireDelay && Input.Down( "Attack1" ) )
		{
			lastFired = 0;
			isAttacking = true;

			Shoot();

			foreach ( var vm in Viewmodels )
			{
				var muzzle = vm.GetAttachmentObject( "muzzle" );
				foreach (var emitter in muzzle.GetComponentsInChildren<ParticleEmitter>())
				{
					emitter.Emit(emitter.GetComponent<ParticleEffect>());
				}
			}
			foreach ( var vm in Worldmodels )
			{
				var muzzle = vm.GetAttachmentObject( "muzzle" );
				foreach ( var emitter in muzzle.GetComponentsInChildren<ParticleEmitter>() )
				{
					emitter.Emit( emitter.GetComponent<ParticleEffect>() );
				}
			}
		}

		foreach ( var vm in Viewmodels )
		{
			vm.Set( "b_attack", isAttacking );
			var player = vm.GetComponentInParent<PlayerController>();
			var move_vectors = player.WishVelocity;
			vm.Set( "move_bob", move_vectors.IsNearlyZero() ? 0.0f : 1.0f );
			vm.Set( "move_x", move_vectors.x );
			vm.Set( "move_y", move_vectors.y );
			vm.Set( "move_z", move_vectors.z );
		}
		foreach ( var vm in Playermodels )
		{
			vm.Set( "b_attack", isAttacking );
		}

		DrawReticules();

		// DrawTraces();

		/*
		foreach ( var player in Scene.Components.GetAll<PlayerController>( FindMode.EnabledInSelfAndDescendants ) )
		{
			if ( !player.GetComponent<Health>().IsAlive ) continue;

			var cam = player.GetComponentInChildren<CameraComponent>();

			var trace = Scene.Trace.Ray( cam.WorldTransform.ForwardRay, 5000f )
				.WithoutTags( "player" )
				.UseHitPosition( true )
				.Run();

			if (trace.Hit)
			{
				DebugOverlay.Line( cam.WorldPosition, trace.HitPosition );
			}

			for ( var offset = -15; offset < 15; offset += 1 )
			{
				var offsetRay = new Ray( cam.WorldPosition, (cam.WorldRotation * Rotation.FromPitch( offset )).Forward );

				var newTrace = Scene.Trace.Ray( offsetRay, 5000f )
					.WithoutTags( "player" )
					.UseHitPosition( true )
					.Run();

				if ( newTrace.Hit )
				{
					DebugOverlay.Line( cam.WorldPosition, newTrace.HitPosition );
				}
			}
		}
		*/
	}

	void FindViewmodels()
	{
		Viewmodels = Scene.Components.GetAll<SkinnedModelRenderer>( FindMode.EnabledInSelfAndDescendants )
			.Where( m => m.GameObject.Name == "v_mp5" )
			.ToList();

		var cameraControllers = Scene.Components.GetAll<LocalCameraControl>( FindMode.EnabledInSelfAndDescendants );

		foreach ( var vm in Viewmodels )
		{
			vm.Set( "b_deploy_skip", true );
			foreach ( var cam in cameraControllers )
			{
				if ( vm.GameObject.IsAncestor( cam.GameObject ) ) continue;

				vm.Tags.Set( cam.ExcludeRenderTag, true );
			}
		}
	}

	void FindWorldmodels()
	{
		Worldmodels = Scene.Components.GetAll<ModelRenderer>( FindMode.EnabledInSelfAndDescendants )
			.Where( m => m.GameObject.Name == "w_mp5" )
			.ToList();
	}

	void FindPlayermodels()
	{
		Playermodels = Scene.Components.GetAll<SkinnedModelRenderer>( FindMode.EnabledInSelfAndDescendants )
			.Where( m => m.GameObject.Name == "Body" )
			.ToList();
	}

	void FindCameras()
	{
		Cameras = Scene.Components.GetAll<PlayerController>( FindMode.EnabledInSelfAndChildren )
			.Select( pc => pc.GetComponentInChildren<CameraComponent>() )
			.ToList();
	}

	void IPlayerReplicatorEvents.PlayerSpawned( GameObject player )
	{
		// find viewmodel
		var viewmodel = player.Components.GetAll<SkinnedModelRenderer>( FindMode.EnabledInSelfAndDescendants )
			.First( m => m.GameObject.Name == "v_mp5" );
		Viewmodels.Add( viewmodel );

		var cameraControllers = Scene.Components.GetAll<LocalCameraControl>( FindMode.EnabledInSelfAndDescendants );

		foreach ( var vm in Viewmodels )
		{
			foreach ( var cam in cameraControllers )
			{
				if ( vm.GameObject.IsAncestor( cam.GameObject ) ) continue;

				vm.Tags.Set( cam.ExcludeRenderTag, true );
			}
		}

		// find worldmodel
		var worldmodel = player.Components.GetAll<ModelRenderer>( FindMode.EnabledInSelfAndDescendants )
			.First( m => m.GameObject.Name == "w_mp5" );
		Worldmodels.Add( worldmodel );

		FindPlayermodels();
		FindCameras();
	}
	void IPlayerHealthEvent.OnDied( GameObject player )
	{
		Viewmodels.Remove( Viewmodels.Find( smr => smr.GameObject.IsAncestor( player ) ) );
		Worldmodels.Remove( Worldmodels.Find( mr => mr.GameObject.IsAncestor( player ) ) );
		Playermodels.Remove( Playermodels.Find( smr => smr.GameObject.IsAncestor( player ) ) );
		Cameras.Remove( Cameras.Find( cam => cam.GameObject.IsAncestor( player ) ) );
	}

	/*
	List<SceneTraceResult> traces = [];
	SceneTraceResult? hitPosition = null;
	List<Vector3> Breakpoints = [];
	Ray XhRay;
	List<Line> CyanLines;
	List<Line> GreenLines;
	List<Line> RedLines;

	void DrawTraces()
	{
		foreach (var trace in traces)
		{
			var forward = Rotation.FromToRotation( trace.StartPosition, trace.HitPosition );
			DebugOverlay.Line( new Line( trace.StartPosition + forward.Forward, trace.HitPosition ) );
		}

		var xforward = Rotation.FromToRotation( XhRay.Position, XhRay.Position + XhRay.Forward );
		DebugOverlay.Line( new Line( XhRay.Position + xforward.Forward, XhRay.Position + XhRay.Forward * 5000f ), Color.Cyan );
		foreach (var bp in Breakpoints)
		{
			DebugOverlay.Sphere( new Sphere( bp, 5 ), Color.Cyan );
		}

		if (hitPosition.HasValue)
		{
			DebugOverlay.Sphere( new Sphere( hitPosition.Value.HitPosition, 5 ) );
		}

		foreach (var line in CyanLines)
		{
			DebugOverlay.Line( line, Color.Cyan );
		}

		foreach ( var line in RedLines )
		{
			DebugOverlay.Line( line, Color.Red );
		}

		foreach ( var line in GreenLines )
		{
			DebugOverlay.Line( line, Color.Green );
		}
	}
	*/

	SceneTraceResult? MostFittingEnemy(Ray ray)
	{
		// shoot a thin, big cylinder that only hits enemies to see if we can autoaim
		List<SceneTraceResult> traces = Scene.Trace.Cylinder( 200f, 10f, ray, 5000f )
				.WithAnyTags( "enemy" )
				.UseHitPosition( true )
				.RunAll()
				.ToList();
		/*
		Breakpoints = [];
		XhRay = ray;
		CyanLines = [];
		GreenLines = [];
		RedLines = [];
		*/

		/*
		while (true)
		{
			var incomplete = Scene.Trace.Cylinder( 200f, 10f, ray, 5000f )
				.WithAnyTags( "enemy" )
				.UseHitPosition( true );

			foreach ( var otherTrace in traces )
			{
				incomplete = incomplete.IgnoreGameObject( otherTrace.GameObject );
			}

			var trace = incomplete.Run();

			if ( !trace.Hit ) break;

			traces.Add( trace );
		}
		*/

		if ( traces.Count == 0 ) return null;

		var tracesWithDistances = 
			traces.Select( trace =>
			{
				// find out how far from the crosshair we are
				var breakPoint = ray.Project( trace.Distance );
				var distance = breakPoint.DistanceSquared( trace.HitPosition );

				return (trace, distance, breakPoint);
			} ).ToList();
		tracesWithDistances.Sort( ( a, b ) => a.Item2.CompareTo( b.Item2 ) );

		// SceneTraceResult? best = null;

		foreach (var trace in tracesWithDistances)
		{
			// Breakpoints.Add( trace.breakPoint );
			// Log.Info( $"distance {trace.Item2} hit {trace.trace.GameObject.Name}" );
			// if ( best.HasValue ) continue;

			// we want to hit it as close to the crosshair as possible
			var traceFromBreakPoint = Scene.Trace.Ray( trace.breakPoint, trace.trace.GameObject.WorldPosition + new Vector3(0, 0, 8) )
				.WithTag( "enemy" )
				.Run();

			if ( !traceFromBreakPoint.Hit ) continue;

			// CyanLines.Add( new Line( traceFromBreakPoint.StartPosition, traceFromBreakPoint.HitPosition ) );
			// Log.Info( $"brp {traceFromBreakPoint.HitPosition}" );

			// now, make sure we can actually hit that
			var directTrace = Scene.Trace.Ray(ray.Position, traceFromBreakPoint.HitPosition)
				.WithoutTags( "player" )
				.UseHitPosition( true )
				.IgnoreGameObject(trace.trace.GameObject)
				.Run();

			if ( directTrace.Hit )
			{
				// Log.Info( "direct trace hit something else" );
				// RedLines.Add( new Line( ray.Position, traceFromBreakPoint.HitPosition ) );
				continue;
			}

			// GreenLines.Add( new Line( ray.Position, traceFromBreakPoint.HitPosition ) );

			var tr = trace.trace;
			tr.HitPosition = traceFromBreakPoint.HitPosition;
			return tr;
		}

		return null;
	}

	void Shoot()
	{
		var playedSound = false;

		foreach ( var player in Scene.GetAll<PlayerController>() )
		{
			if ( !player.GetComponent<Health>().IsAlive ) continue;

			if ( !playedSound )
			{
				Sound.Play( FireSound, player.WorldPosition );
				playedSound = true;
			}

			var cam = player.GetComponentInChildren<CameraComponent>();

			var pitch = 0f;

			var sideToSide = Rotation.FromYaw( Game.Random.Float( -1, 1 ) );

			var ray = new Ray( cam.WorldPosition, (cam.WorldRotation * sideToSide).Forward );

			var trace = MostFittingEnemy( ray );

			// hitPosition = trace;

			if (!trace.HasValue)
			{
				// if we didn't hit an enemy, just shoot straight forwards for wall impacts
				trace = Scene.Trace.Ray( ray, 5000f )
					.WithoutTags( "player" )
					.UseHitPosition( true )
					.Run();
			}

			IDamageable damageable = null;

			if ( trace.Value.Hit ) damageable = trace.Value.GameObject.Components.Get<IDamageable>();

			// TODO
			player.Components.GetAll<SkinnedModelRenderer>().First( sm => sm.GameObject.Name == "v_mp5" ).Set( "aim_pitch_inertia", pitch );

			if ( damageable is not null )
			{
				damageable.OnDamage( new DamageInfo()
				{
					Damage = Damage,
					Attacker = GameObject,
					Position = trace.Value.HitPosition,
				} );
			}

			if ( !trace.HasValue || !trace.Value.Hit ) return;

			if (TempImpact is not null)
			{
				var impact = TempImpact.Clone( trace.Value.HitPosition, Rotation.LookAt( trace.Value.Normal ) );
				impact.SetParent( trace.Value.GameObject );
				impact.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 120;
			}
			else
			{
				var prefab = trace.Value.Surface.PrefabCollection.BulletImpact;

				if ( prefab is not null )
				{
					var impact = prefab.Clone( trace.Value.HitPosition, trace.Value.Normal.EulerAngles.ToRotation().Inverse );
					impact.SetParent( trace.Value.GameObject );
					impact.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 120;
				}

				if ( trace.Value.Surface.SoundCollection.Bullet is not null ) Sound.Play( trace.Value.Surface.SoundCollection.Bullet, trace.Value.HitPosition );
			}

			/*
			var impact = new GameObject(trace.GameObject, false);
			var particle = impact.AddComponent<LegacyParticleSystem>();
			Log.Info( trace.Surface.ImpactEffects.Bullet[0] );
			var system = ParticleSystem.Load( trace.Surface.ImpactEffects.Bullet[0] );
			Log.Info( system );
			particle.Particles = system;
			impact.WorldPosition = trace.HitPosition;
			impact.WorldRotation = Rotation.LookAt( trace.Normal );
			impact.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 120;
			impact.Enabled = true;
			*/

		}
	}

	void DrawReticules()
	{
		if ( Crosshair is null || !Crosshair.IsValid ) return;

		foreach ( var cam in Cameras )
		{
			var hud = cam.Hud;

			var screenRect = cam.ScreenRect;

			hud.DrawTexture( Crosshair, new Rect( -40, -20, 80, 40 ) + screenRect.Center, Color.FromRgba( 0xFFFFFF50 ) );
		}
	}
}