Weapons/MeleeWeapon.cs
using Opium;
using System.Diagnostics.Metrics;

public sealed class MeleeWeapon : BaseWeapon
{
	/// <summary>
	/// The trace radius for the arc.
	/// </summary>
	[Property] public float ArcTraceRadius { get; set; } = 1f;

	/// <summary>
	/// Durability of the weapon
	/// </summary>
	[Property] public int Durability { get; set; } = 100;

	/// <summary>
	/// How much durability should we take on hit?
	/// </summary>
	[Property] public int DurabilityTakenOnHit { get; set; } = 10;

	/// <summary>
	/// The cooldown between aiming.
	/// </summary>
	[Property] public float AimCooldown { get; set; } = 1.5f;

	[Property] public float AttackCooldown { get; set; } = 1f;

	/// <summary>
	/// Does the arc open doors?
	/// </summary>
	[Property] public bool HitOpenDoor { get; set; } = true;

	public bool IsBroken => Durability <= 0;

	/// <summary>
	/// This is a bit shit.
	/// </summary>
	public WeaponArcOrigin ArcOrigin => Actor.CameraObject.Components.Get<WeaponArcOrigin>( FindMode.EverythingInSelfAndDescendants );

	/// <summary>
	/// Accessor for attacks
	/// </summary>
	public IEnumerable<MeleeWeaponAttack> Attacks => this.IsValid() ?
		Components.GetAll<MeleeWeaponAttack>( FindMode.EverythingInSelfAndDescendants ) : new List<MeleeWeaponAttack>();

	public MeleeWeaponAttack CurrentAttack => Attacks.FirstOrDefault( x => x.IsValid() && x.IsActive );

	public int CurrentIndex = 0;
    [Property] public List<MeleeWeaponAttack> MainAttacks { get; set; }
    [Property, ReadOnly] public MeleeWeaponAttack MainAttack { get; set; }
	[Property] public MeleeWeaponAttack BlockAttack { get; set; }
	 
	[Property] public float BlockDamageFactor { get; set; } = 0f;

	/// <summary>
	/// How much posture damage does this weapon do when hitting someone?
	/// </summary>
	[Property, Group( "Posture" )] public float Posture { get; set; } = 25f;

	protected override void OnEnabled()
	{
		MainAttack = MainAttacks.FirstOrDefault();
		CurrentIndex = 0;
	}

	public override void Shoot()
	{
		var idx = CurrentIndex;

		if ( idx > MainAttacks.Count - 1 )
		{
			idx = 0;
			CurrentIndex = 0;
		}

		Actor.TriggerEvent( "shoot", this );

		var attack = MainAttacks[idx];
		MainAttack = attack;

		attack?.Activate();

		CurrentIndex++;
	}

	public override void Aim()
	{
		base.Aim();

		Actor.TriggerEvent( "aim", this );

		BlockAttack?.Activate();
	}

    /// <summary>
    /// Can we aim? In this case, it's probably a block attack. That or it's an alternate attack..
    /// </summary>
    /// <returns></returns>
	public override bool CanAim()
	{
		if ( !Actor.CanAim( this ) ) return false;
        if ( CurrentAttack is not null ) return false;
		if ( TimeSinceShoot < AttackCooldown ) return false;
		if ( Attacks.Any( x => x.IsActive ) ) return false;

        return TimeSinceAim > AimCooldown;
	}

	public bool TestHit( Vector3 arcPosition, out SceneTraceResult tr, float delta = 1f )
	{
		tr = Scene.Trace.Ray( ArcOrigin.Transform.World.Position, arcPosition )
			.IgnoreGameObjectHierarchy( GameObject.Root )
			.UseHitboxes( true )
			.Size( ArcTraceRadius * delta )
			.Run();

		if ( tr.Hit && tr.GameObject is not null )
		{
			return true;
		}

		return false;
	}

	public void DoMeleeHit( SceneTraceResult tr )
	{
		// Inflict damage on whatever we find.
		var damageInfo = Opium.DamageInfo.Generic( BaseDamage, Actor?.GameObject ?? GameObject, GameObject, "melee", HitOpenDoor ? "open_door" : "" );
		CalculateDamage( damageInfo );

		tr.GameObject.TakeDamage( damageInfo );

		bool isRicochet = tr.GameObject.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) is not null && damageInfo.Damage == 0;
		var metal = ResourceLibrary.Get<Surface>( "surface/world/op_metal.surface" );

		Surface surface = isRicochet ? metal : GetSurfaceFromTrace( tr );

		// If it's an actor and the damage is zero, do nothing
		var rb = tr.GameObject.Components.Get<Rigidbody>( FindMode.EnabledInSelfAndChildren );
		if ( rb is not null )
		{
			rb.ApplyImpulseAt( tr.EndPosition, tr.Normal * 10000f );
		}

		var skinnedModelRenderer = tr.GameObject.Components.Get<SkinnedModelRenderer>();
		if ( skinnedModelRenderer is not null && tr.Hitbox is not null && tr.Hitbox.Bone is not null )
		{
			CreateImpactEffects( skinnedModelRenderer.GetBoneObject( tr.Hitbox.Bone ), surface, tr.EndPosition, tr.Normal );
		}
		else
			CreateImpactEffects( tr.GameObject, surface, tr.EndPosition, tr.Normal );

		// Only take durability if we hit an actor
		if ( tr.GameObject.Tags.Has( "actor" ) ) 
		{
			Durability -= DurabilityTakenOnHit;
			Actor.TriggerEvent( "durability_loss", Durability );
		}

		if ( Durability <= 0 )
		{
			DropAsync();
		}
	}

	// TODO: This fucking sucks!!!!!
	async void DropAsync()
	{
		await GameTask.DelaySeconds( 0.25f );
		
		if ( !this.IsValid() )
			return;

		if ( Actor.IsValid() )
			return;

		var wpn = Actor.DropWeapon( this );

		if ( !wpn.IsValid() )
			return;

		var rigidbody = wpn.Components.Get<Rigidbody>( FindMode.EverythingInSelfAndChildren );
		if ( !rigidbody.IsValid() )
			return;

		rigidbody.ApplyImpulse( Vector3.Random * 10000f );
	}

	/// <summary>
	/// Overriden.
	/// </summary>
	/// <returns></returns>
	public override bool CanShoot()
	{
		if ( !base.CanShoot() ) return false;
		if ( TimeSinceShoot < AttackCooldown ) return false;
		if ( Durability <= 0 ) return false;

		// Can only run primary attack if one is not active.
		return !Attacks.Any( x => x.IsActive );
	}
}