combat/fighter_combat.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;

public sealed class FighterCombat : Component
{
	[Property, Group( "Debug" )] public bool ShowDebugTraces { get; set; } = true;
	[Property, Group( "Debug" )] public float DebugTraceDuration { get; set; } = 1.5f;

	[Property, Group( "Équipement" )] public string WeaponAttachmentName { get; set; } = "weapon_socket";
	[Property, Group( "Équipement" )] public string ShieldAttachmentName { get; set; } = "shield_socket";
	[Property, Group( "Équipement" )] public SkinnedModelRenderer PreferredRenderer { get; set; }

	[Property, Group( "Équipement" )] public Weapon MainWeapon { get; private set; }
	[Property, Group( "Équipement" )] public Weapon ShieldItem { get; private set; }

	public Weapon CurrentWeapon => MainWeapon ?? ShieldItem;
	public bool HasShieldEquipped => ShieldItem != null && ShieldItem.IsValid;

	[Property, Group( "Profils de Combat" )] public WeaponComboResource UnarmedProfile { get; set; }

	[Property, Group( "Charge & Attaque Lourde" )] public float HeavyChargeThreshold { get; set; } = 0.35f;
	[Property, Group( "Charge & Attaque Lourde" )] public float HeavyDamageMultiplier { get; set; } = 1.5f;
	[Property, Group( "Charge & Attaque Lourde" )] public float HeavyBalanceMultiplier { get; set; } = 1.75f;
	[Property, Group( "Charge & Attaque Lourde" )] public float HeavyKnockbackMultiplier { get; set; } = 1.4f;

	// =========================================================================
	// AUDIO UNIVERSEL DU COMBATTANT (EFFORT PHYSIQUE / VOCAL)
	// =========================================================================
	[Property, Group( "Audio Combattant (Effort)" )]
	public SoundEvent AttackAnticipationSound { get; set; }

	[Property, Group( "Audio Combattant (Effort)" )]
	public SoundEvent AttackReleaseSound { get; set; }

	// =========================================================================
	// AUDIO UNIVERSEL DU KICK (DÉCOR, CHAIR, SWING)
	// =========================================================================
	[Property, Group( "Audio Kick Universel" )]
	[Description( "Son joué lorsque le kick frappe un combattant (impact lourd de botte contre chair)." )]
	public SoundEvent KickHitFleshSound { get; set; }

	[Property, Group( "Audio Kick Universel" )]
	[Description( "Son joué lorsque le kick percute le décor, un mur ou un prop." )]
	public SoundEvent KickHitWorldSound { get; set; }

	[Property, Group( "Audio Kick Universel" )]
	[Description( "Son joué lorsque le kick fend l'air." )]
	public SoundEvent KickSwingSound { get; set; }

	// =========================================================================
	// KICK VOLUMÉTRIQUE (PROFIL UNIQUE ET INVIOLABLE)
	// =========================================================================
	[Property, Group( "Kick Volumétrique" )]
	public AttackProfile DefaultKickProfile { get; set; } = new()
	{
		Name = "Spartan Kick",
		Damage = 10f,
		BalanceDamage = 65f,
		Range = 95f,
		Radius = 22f,
		FighterKnockbackForce = 180f,
		FighterUpwardLift = 0.0f,
		PropPhysicsImpulse = 24000f,
		PropUpwardLift = 0.35f,
		OriginOffset = new Vector3( 10f, 0f, -20f ),
		WindupDelay = 0.10f,
		Cooldown = 0.85f,
		AnimIndex = 99,
		IsKickAnim = true,
		AffectWholeBody = true,
		FullBodyDuration = 0.85f,
		CausesKnockdownOnStagger = true,
		Shape = AttackShape.HorizontalSweep,
		RayCount = 3,
		SweepAngle = 35f
	};

	[Property, Group( "Timing" )] public float ComboResetWindow { get; set; } = 1.35f;

	private bool IsPlayer => _isPlayerCached ??= (Components.Get<PlayerController>() != null);
	private bool? _isPlayerCached;

	public WeaponComboResource ActiveProfile
	{
		get
		{
			if ( !IsPlayer && CurrentWeapon?.BotComboData != null )
				return CurrentWeapon.BotComboData;

			return CurrentWeapon?.ComboData ?? ShieldItem?.ComboData ?? UnarmedProfile;
		}
	}

	public bool IsAnticipating => _pendingProfile != null || IsThrowAnticipating;
	public bool IsThrowAnticipating { get; private set; } = false;
	public bool IsExecutingThrow { get; private set; } = false;
	public float CurrentChargeTime { get; private set; } = 0f;
	public float ChargeRatio => Math.Clamp( CurrentChargeTime / MathF.Max( 0.01f, HeavyChargeThreshold ), 0f, 1f );
	public bool IsExecutingStrike => _isExecutingStrike;

	private AttackProfile _pendingProfile;
	private bool _pendingIsCombo = false;
	private int _currentComboIndex = 0;
	private TimeSince _timeSinceLastAttack = 10f;
	private bool _isExecutingStrike = false;
	private Fighter _fighter;
	private FighterMovement _movement;
	private PlayerController _playerController;
	private int _throwActionToken = 0;

	private readonly List<SkinnedModelRenderer> _cachedRenderers = new();

	// Cache pour éliminer les allocations GC en boucle de combat
	private readonly HashSet<Fighter> _hitFightersCache = new();
	private readonly HashSet<Weapon> _hitWeaponsCache = new();
	private readonly HashSet<PhysicsBody> _hitBodiesCache = new();

	protected override void OnAwake()
	{
		_fighter = Components.Get<Fighter>();
		_movement = Components.Get<FighterMovement>();
		_playerController = Components.Get<PlayerController>();
		RefreshRenderersCache();
	}

	protected override void OnStart()
	{
		RefreshRenderersCache();

		foreach ( var weapon in Components.GetAll<Weapon>( FindMode.InChildren ) )
		{
			if ( !weapon.IsEquipped )
				EquipWeapon( weapon );
		}

		UpdateAnimWeaponType();
	}

	protected override void OnUpdate()
	{
		SyncWeaponsToAttachments();
		UpdateAnticipationTick();
	}

	protected override void OnPreRender()
	{
		SyncWeaponsToAttachments();
	}

	public void RefreshRenderersCache()
	{
		_cachedRenderers.Clear();
		_cachedRenderers.AddRange( Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ) );
	}

	public void NotifyParrySuccess( Vector3 hitPosition )
	{
		var parrySound = ActiveProfile?.ParrySound ?? ShieldItem?.ComboData?.ParrySound;
		if ( parrySound != null )
			Sound.Play( parrySound, hitPosition );
	}

	public void NotifyBlockSuccess( Vector3 hitPosition, bool hasShield )
	{
		SoundEvent blockSound = null;
		if ( hasShield && ShieldItem?.ComboData?.BlockSound != null )
			blockSound = ShieldItem.ComboData.BlockSound;
		else if ( ActiveProfile?.BlockSound != null )
			blockSound = ActiveProfile.BlockSound;
		else if ( ActiveProfile?.HitWorldSound != null )
			blockSound = ActiveProfile.HitWorldSound;

		if ( blockSound != null )
			Sound.Play( blockSound, hitPosition );
	}

	public bool StartPrimaryAttack( Vector3 eyePos, Vector3 aimDir )
	{
		if ( IsExecutingThrow ) return false;

		var combo = ActiveProfile?.PrimaryCombo;
		if ( combo == null || combo.Count == 0 ) return false;

		if ( _timeSinceLastAttack > ComboResetWindow && !_isExecutingStrike )
		{
			_currentComboIndex = 0;
		}

		var profile = combo[_currentComboIndex];
		return StartAction( profile, isCombo: true, eyePos, aimDir );
	}

	public bool StartKick( Vector3 eyePos, Vector3 aimDir )
	{
		if ( IsExecutingThrow ) return false;

		return StartAction( DefaultKickProfile, isCombo: false, eyePos, aimDir );
	}

	private bool StartAction( AttackProfile profile, bool isCombo, Vector3 eyePos, Vector3 aimDir )
	{
		if ( profile == null || _fighter == null || _fighter.IsDead || _fighter.Status != FighterStatus.Active )
			return false;

		if ( IsAnticipating )
			return false;

		_pendingProfile = profile;
		_pendingIsCombo = isCombo;
		CurrentChargeTime = 0f;
		_timeSinceLastAttack = 0f;

		BroadcastAnticipationState( active: true, profile, isHeavy: profile.IsHeavy, ratio: 0f );

		if ( profile.IsKickAnim && IsPlayer )
		{
			CombatEvents.EmitPlayerKickWindup();
		}

		if ( AttackAnticipationSound != null )
			Sound.Play( AttackAnticipationSound, eyePos );

		var swingSound = profile.IsKickAnim ? (KickSwingSound ?? profile.SwingSound) : profile.SwingSound;
		if ( swingSound != null )
			Sound.Play( swingSound, eyePos );

		return true;
	}

	private void UpdateAnticipationTick()
	{
		if ( !IsAnticipating ) return;

		if ( _fighter == null || _fighter.IsDead || _fighter.Status != FighterStatus.Active )
		{
			CancelAnticipation();
			return;
		}

		CurrentChargeTime += Time.Delta;
		_timeSinceLastAttack = 0f;

		if ( _pendingProfile != null )
		{
			bool isHeavyNow = _pendingProfile.IsHeavy || (CurrentChargeTime >= HeavyChargeThreshold);
			BroadcastAnticipationState( active: true, _pendingProfile, isHeavyNow, ChargeRatio );
		}
		else if ( IsThrowAnticipating )
		{
			BroadcastThrowAnticipation( active: true, ChargeRatio );
		}
	}

	public async void ReleaseAction( Vector3 eyePos, Vector3 aimDir )
	{
		if ( !IsAnticipating || _pendingProfile == null ) return;

		var profile = _pendingProfile;
		bool isCombo = _pendingIsCombo;
		bool isHeavy = profile.IsHeavy || (CurrentChargeTime >= HeavyChargeThreshold);

		if ( profile.IsKickAnim && IsPlayer )
		{
			CombatEvents.EmitPlayerKickReleased();
		}

		_pendingProfile = null;
		_pendingIsCombo = false;

		BroadcastReleaseState( profile, isHeavy );

		if ( AttackReleaseSound != null )
			Sound.Play( AttackReleaseSound, eyePos );

		if ( isCombo )
		{
			var combo = ActiveProfile?.PrimaryCombo;
			if ( combo != null && combo.Count > 0 )
				_currentComboIndex = (_currentComboIndex + 1) % combo.Count;
		}

		while ( _isExecutingStrike && IsValid )
		{
			await Task.Frame();
		}

		_timeSinceLastAttack = 0f;
		_ = ExecuteStrikeAsync( profile, eyePos, aimDir, isHeavy );
	}

	public bool StartThrow()
	{
		if ( CurrentWeapon == null || _fighter == null || _fighter.IsDead || _fighter.Status != FighterStatus.Active )
			return false;

		if ( IsAnticipating || IsExecutingThrow || _isExecutingStrike )
			return false;

		IsThrowAnticipating = true;
		CurrentChargeTime = 0f;
		_timeSinceLastAttack = 0f;

		BroadcastThrowAnticipation( active: true, 0f );

		if ( AttackAnticipationSound != null )
			Sound.Play( AttackAnticipationSound, WorldPosition + Vector3.Up * 50f );

		return true;
	}

	public void ReleaseThrow( Vector3 aimDir, float speed )
	{
		if ( !IsThrowAnticipating || CurrentWeapon == null ) return;

		IsThrowAnticipating = false;
		IsExecutingThrow = true;
		_throwActionToken++;
		int currentToken = _throwActionToken;

		Weapon weaponToThrow = MainWeapon ?? ShieldItem;
		var profile = weaponToThrow.ComboData?.ThrownProfile ?? new AttackProfile();

		BroadcastThrowRelease();

		if ( AttackReleaseSound != null )
			Sound.Play( AttackReleaseSound, WorldPosition + Vector3.Up * 50f );

		_ = ExecuteThrowSequenceAsync( weaponToThrow, profile, aimDir, speed, currentToken );
	}

	private async Task ExecuteThrowSequenceAsync( Weapon weaponToThrow, AttackProfile profile, Vector3 aimDir, float speed, int token )
	{
		if ( profile.WindupDelay > 0f )
			await Task.Delay( (int)(profile.WindupDelay * 1000f) );

		if ( !IsValid || token != _throwActionToken || weaponToThrow == null || !weaponToThrow.IsValid )
		{
			IsExecutingThrow = false;
			return;
		}

		string socket = (weaponToThrow == MainWeapon || ShieldItem == null) ? WeaponAttachmentName : ShieldAttachmentName;
		var attach = ResolveAttachment( socket );
		var spawnPos = attach?.Position ?? (_fighter != null ? _fighter.WorldPosition + Vector3.Up * 50f + aimDir * 25f : weaponToThrow.WorldPosition);
		var spawnRot = Rotation.LookAt( aimDir );

		if ( weaponToThrow == MainWeapon )
			MainWeapon = null;
		else
			ShieldItem = null;

		weaponToThrow.Drop( aimDir * speed, isLethalThrow: true, dropPosition: spawnPos, dropRotation: spawnRot );

		float remainingRecovery = MathF.Max( 0.05f, profile.Cooldown - profile.WindupDelay );
		await Task.Delay( (int)(remainingRecovery * 1000f) );

		if ( !IsValid || token != _throwActionToken ) return;

		IsExecutingThrow = false;
		_currentComboIndex = 0;
		RefreshRenderersCache();

		BroadcastThrowReset();
		UpdateAnimWeaponType();
	}

	public void CancelAnticipation()
	{
		if ( _pendingProfile?.IsKickAnim == true && IsPlayer )
		{
			CombatEvents.EmitPlayerKickReleased();
		}

		_pendingProfile = null;
		_pendingIsCombo = false;
		IsThrowAnticipating = false;
		CurrentChargeTime = 0f;
		_throwActionToken++;

		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "attack_anticipating", false );
			rend.Set( "attack_released", false );
			rend.Set( "is_throwing", false );
			rend.Set( "charge_ratio", 0f );
			rend.Set( "is_kicking", false );
			rend.Set( "attack", false );
			rend.Set( "kick", false );
			rend.Set( "is_full_body_attack", false );
			rend.Set( "full_body_weight", 0.0f );
		}
	}

	public void ExecuteSpecificAttack( AttackProfile profile, Vector3 eyePos, Vector3 aimDir )
	{
		if ( StartAction( profile, isCombo: false, eyePos, aimDir ) )
			ReleaseAction( eyePos, aimDir );
	}

	public void AttackDirect( Vector3 eyePos, Vector3 aimDir )
	{
		if ( StartPrimaryAttack( eyePos, aimDir ) )
			ReleaseAction( eyePos, aimDir );
	}

	public void KickDirect( Vector3 eyePos, Vector3 aimDir )
	{
		if ( StartKick( eyePos, aimDir ) )
			ReleaseAction( eyePos, aimDir );
	}

	public void Attack( Vector3 eyePos, Vector3 aimDir ) => AttackDirect( eyePos, aimDir );
	public void Kick( Vector3 eyePos, Vector3 aimDir ) => KickDirect( eyePos, aimDir );
	public void StartAttackAnticipation( Vector3 eyePos, Vector3 aimDir ) => StartPrimaryAttack( eyePos, aimDir );
	public void ReleaseAttack( Vector3 eyePos, Vector3 aimDir ) => ReleaseAction( eyePos, aimDir );
	public void CancelAttackAnticipation() => CancelAnticipation();

	private async Task ExecuteStrikeAsync( AttackProfile profile, Vector3 eyePos, Vector3 aimDir, bool isHeavy )
	{
		_isExecutingStrike = true;
		BroadcastAttackState( profile, active: true, isHeavy );

		if ( profile.WindupDelay > 0f )
			await Task.Delay( (int)(profile.WindupDelay * 1000f) );

		if ( !IsValid || _fighter == null || _fighter.IsDead || _fighter.Status != FighterStatus.Active )
		{
			BroadcastAttackState( profile, active: false, isHeavy );
			_isExecutingStrike = false;
			return;
		}

		PerformAttackTrace( profile, eyePos, aimDir, isHeavy );

		// Priorité à FullBodyDuration pour le kick si renseigné, sinon repli sur le Cooldown
		float animDuration = (profile.IsKickAnim && profile.FullBodyDuration > 0f)
			? profile.FullBodyDuration
			: profile.Cooldown;

		var waitTime = MathF.Max( 0.05f, animDuration - profile.WindupDelay );
		await Task.Delay( (int)(waitTime * 1000f) );

		if ( IsValid )
			BroadcastAttackState( profile, active: false, isHeavy );

		// Si le cooldown complet dépasse l'animation, on attend la fin de la fenêtre de récupération
		if ( profile.Cooldown > animDuration )
		{
			await Task.Delay( (int)((profile.Cooldown - animDuration) * 1000f) );
		}

		_isExecutingStrike = false;
		_timeSinceLastAttack = 0f;
	}

	private void PerformAttackTrace( AttackProfile profile, Vector3 eyePos, Vector3 aimDir, bool isHeavy )
	{
		var aimRotation = Rotation.LookAt( aimDir );
		var attackOrigin = ResolveOrigin( profile, eyePos, aimRotation );

		_hitFightersCache.Clear();
		_hitWeaponsCache.Clear();
		_hitBodiesCache.Clear();

		bool worldImpactPlayed = false;
		bool hitAnything = false;

		foreach ( var rayDir in profile.GenerateRays( aimDir ) )
		{
			var targetEnd = attackOrigin + rayDir * profile.Range;

			var allHits = Scene.Trace.Sphere( profile.Radius, attackOrigin, targetEnd )
				.UseHitboxes()
				.IgnoreGameObjectHierarchy( GameObject )
				.RunAll();

			if ( ShowDebugTraces )
			{
				var primaryTrace = Scene.Trace.Sphere( profile.Radius, attackOrigin, targetEnd )
					.UseHitboxes()
					.IgnoreGameObjectHierarchy( GameObject )
					.Run();
				DebugOverlay.Trace( primaryTrace, DebugTraceDuration, overlay: true );
			}

			foreach ( var trace in allHits )
			{
				if ( !trace.Hit ) continue;

				hitAnything = true;

				var victim = trace.GameObject?.Components.Get<Fighter>( FindMode.EverythingInSelfAndAncestors )
						  ?? trace.GameObject?.Components.GetInParent<Fighter>();

				// 1. Touche un combattant ennemi
				if ( victim != null && victim != _fighter && !victim.IsDead )
				{
					if ( _hitFightersCache.Add( victim ) )
					{
						var fleshSound = profile.IsKickAnim 
							? (KickHitFleshSound ?? profile.HitSoundOverride) 
							: (profile.HitSoundOverride ?? ActiveProfile?.HitFleshSound);

						if ( fleshSound != null )
							Sound.Play( fleshSound, trace.HitPosition );

						if ( profile.HitParticlePrefab != null )
							profile.HitParticlePrefab.Clone( trace.HitPosition, Rotation.LookAt( trace.Normal ) );

						ApplyHitToVictim( profile, victim, trace, rayDir, isHeavy );
					}
					continue;
				}

				// 2. Touche une arme au sol (Kick & Pop)
				var weapon = trace.GameObject?.Components.Get<Weapon>( FindMode.EverythingInSelfAndAncestors );
				if ( weapon != null && !weapon.IsEquipped && profile.IsKickAnim )
				{
					if ( _hitWeaponsCache.Add( weapon ) )
						weapon.ReceiveKick( attackOrigin, rayDir, _fighter );
					continue;
				}

				// 3. Touche un prop physique ou le décor
				var propBody = trace.Body ?? trace.GameObject?.Components.Get<Rigidbody>( FindMode.EverythingInSelfAndAncestors )?.PhysicsBody;
				if ( propBody != null && propBody.BodyType == PhysicsBodyType.Dynamic && _hitBodiesCache.Add( propBody ) )
				{
					if ( trace.GameObject.Components.Get<Fighter>( FindMode.EverythingInSelfAndAncestors ) == null &&
						 trace.GameObject.Components.Get<Weapon>( FindMode.EverythingInSelfAndAncestors ) == null )
					{
						var propLaunchDir = (rayDir + Vector3.Up * profile.PropUpwardLift).Normal;
						var impulseMult = isHeavy ? 1.6f : 1.0f;
						propBody.ApplyImpulseAt( trace.HitPosition, propLaunchDir * (profile.PropPhysicsImpulse * impulseMult) );

						if ( !worldImpactPlayed )
						{
							var worldSound = profile.IsKickAnim 
								? (KickHitWorldSound ?? ActiveProfile?.HitWorldSound) 
								: ActiveProfile?.HitWorldSound;

							if ( worldSound != null )
							{
								Sound.Play( worldSound, trace.HitPosition );
								worldImpactPlayed = true;
							}
						}
					}
				}
				else if ( _hitFightersCache.Count == 0 && !worldImpactPlayed && trace.Surface != null )
				{
					var worldSound = profile.IsKickAnim 
						? (KickHitWorldSound ?? ActiveProfile?.HitWorldSound) 
						: ActiveProfile?.HitWorldSound;

					if ( worldSound != null )
					{
						Sound.Play( worldSound, trace.HitPosition );
						worldImpactPlayed = true;
					}
				}
			}
		}

		// =========================================================================
		// WALL KICK AÉRIEN
		// =========================================================================
		if ( profile.IsKickAnim && hitAnything )
		{
			var movement = _movement ?? Components.Get<FighterMovement>();
			if ( movement != null && !movement.IsGrounded )
			{
				var viewDir = GetCurrentViewDirection( aimDir );
				movement.ApplyWallKick( viewDir );
			}
		}
	}

	private Vector3 GetCurrentViewDirection( Vector3 fallbackDir )
	{
		var player = _playerController ?? Components.Get<PlayerController>();
		if ( player != null )
		{
			if ( player.Camera != null && player.Camera.IsValid )
				return player.Camera.WorldRotation.Forward;

			if ( player.EyePivot != null && player.EyePivot.IsValid )
				return player.EyePivot.WorldRotation.Forward;
		}

		return fallbackDir.LengthSquared > 0.001f ? fallbackDir : WorldRotation.Forward;
	}

	private void ApplyHitToVictim( AttackProfile profile, Fighter victim, SceneTraceResult trace, Vector3 attackRayDir, bool isHeavy )
	{
		var rend = trace.GameObject?.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants )
				?? victim.Components.GetInChildren<SkinnedModelRenderer>();

		var boneName = (rend?.Model != null && trace.Bone >= 0) ? rend.Model.GetBoneName( trace.Bone ) : "Unknown";

		var hitZone = profile.IsKickAnim ? HitZone.Legs : CombatGeometry.ResolveHitZone( boneName );
		var hitDirection = CombatGeometry.CalculateHitDirection( victim.WorldPosition, victim.WorldRotation, WorldPosition );

		var balanceMult = MathX.Lerp( 0.5f, 1.0f, _fighter.BalanceRatio );
		var horizontalDir = attackRayDir.WithZ( 0 ).Normal;
		var fighterPushDir = (horizontalDir + Vector3.Up * profile.FighterUpwardLift).Normal;

		var dmgMult = isHeavy ? HeavyDamageMultiplier : 1.0f;
		var balMult = isHeavy ? HeavyBalanceMultiplier : 1.0f;
		var knockMult = isHeavy ? HeavyKnockbackMultiplier : 1.0f;

		var context = new DamageContext
		{
			Attacker = _fighter,
			HealthDamage = profile.Damage * balanceMult * dmgMult,
			BalanceDamage = profile.BalanceDamage * balanceMult * balMult,
			KnockbackForce = fighterPushDir * (profile.FighterKnockbackForce * knockMult),
			HitPosition = trace.HitPosition,
			Zone = hitZone,
			Direction = hitDirection,
			Type = profile.Type,
			IsHeavy = isHeavy,
			CausesKnockdown = profile.CausesKnockdownOnStagger || profile.IsKickAnim || isHeavy,
			ForceKnockdown = false
		};

		_fighter.AddAdrenaline( isHeavy ? 14f : 8f );
		victim.ApplyDamage( context );

		// Événements d'arène spécifiques au joueur (Rebound Strike & Air Knockdown)
		if ( IsPlayer )
		{
			if ( _movement != null && _movement.IsReboundAirborne )
			{
				_movement.ConsumeReboundAirborne();
				CombatEvents.EmitPlayerReboundStrike( _fighter, victim, context );
			}

			if ( _movement != null && !_movement.IsGrounded && context.CausesKnockdown )
			{
				CombatEvents.EmitPlayerAirKnockdown( _fighter, victim, context );
			}
		}
	}

	private void BroadcastAnticipationState( bool active, AttackProfile profile, bool isHeavy, float ratio )
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "attack_anticipating", active );
			rend.Set( "is_kicking", profile.IsKickAnim );
			rend.Set( "is_throwing", false );
			rend.Set( "is_heavy", isHeavy );
			rend.Set( "charge_ratio", ratio );

			if ( profile.AnimIndex > 0 )
				rend.Set( "attack_index", profile.AnimIndex );

			if ( active )
				rend.Set( "attack_released", false );
		}
	}

	private async void BroadcastReleaseState( AttackProfile profile, bool isHeavy )
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "attack_anticipating", false );
			rend.Set( "attack_released", true );
			rend.Set( "is_kicking", profile.IsKickAnim );
			rend.Set( "is_throwing", false );
			rend.Set( "is_heavy", isHeavy );

			if ( profile.AnimIndex > 0 )
				rend.Set( "attack_index", profile.AnimIndex );
		}

		await Task.Delay( 40 );
		if ( !IsValid ) return;

		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( rend.IsValid )
				rend.Set( "attack_released", false );
		}
	}

	private void BroadcastThrowAnticipation( bool active, float ratio )
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "is_throwing", true );
			rend.Set( "attack_anticipating", active );
			rend.Set( "charge_ratio", ratio );
			if ( active )
				rend.Set( "attack_released", false );
		}
	}

	private async void BroadcastThrowRelease()
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "attack_anticipating", false );
			rend.Set( "attack_released", true );
			rend.Set( "is_throwing", true );
		}

		await Task.Delay( 40 );
		if ( !IsValid ) return;

		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( rend.IsValid )
				rend.Set( "attack_released", false );
		}
	}

	private void BroadcastThrowReset()
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			rend.Set( "is_throwing", false );
			rend.Set( "attack_anticipating", false );
			rend.Set( "attack_released", false );
			rend.Set( "charge_ratio", 0f );
		}
	}

	private void BroadcastAttackState( AttackProfile profile, bool active, bool isHeavy )
	{
		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			if ( active )
			{
				rend.Set( "is_full_body_attack", profile.AffectWholeBody );
				rend.Set( "full_body_weight", profile.AffectWholeBody ? 1.0f : 0.0f );
				rend.Set( "is_heavy", isHeavy );

				if ( profile.IsKickAnim )
				{
					rend.Set( "is_kicking", true );
					rend.Set( "kick", true );
				}
				else if ( profile.AnimIndex > 0 )
				{
					rend.Set( "attack_index", profile.AnimIndex );
					rend.Set( "attack", true );
				}
			}
			else
			{
				rend.Set( "attack", false );
				rend.Set( "kick", false );
				rend.Set( "is_kicking", false );
				rend.Set( "is_full_body_attack", false );
				rend.Set( "full_body_weight", 0.0f );
				rend.Set( "attack_released", false );
			}
		}
	}

	public void SyncWeaponsToAttachments()
	{
		if ( MainWeapon != null && MainWeapon.IsValid && MainWeapon.CurrentState == WeaponPhysicsState.Equipped )
			AttachWeaponTo( MainWeapon, WeaponAttachmentName );

		if ( ShieldItem != null && ShieldItem.IsValid && ShieldItem.CurrentState == WeaponPhysicsState.Equipped )
		{
			string socketToUse = (MainWeapon == null) ? WeaponAttachmentName : ShieldAttachmentName;
			AttachWeaponTo( ShieldItem, socketToUse );
		}
	}

	private void AttachWeaponTo( Weapon weapon, string attachmentName )
	{
		var attachTransform = ResolveAttachment( attachmentName );
		if ( !attachTransform.HasValue ) return;

		var attachRot = attachTransform.Value.Rotation;
		var attachPos = attachTransform.Value.Position;

		var profile = GetWeaponProfile( weapon );
		var posOffset = profile?.GripPositionOffset ?? Vector3.Zero;
		var rotOffset = profile?.GripAnglesOffset ?? Angles.Zero;

		weapon.WorldPosition = attachPos + (attachRot * posOffset);
		weapon.WorldRotation = attachRot * Rotation.From( rotOffset );
	}

	private WeaponComboResource GetWeaponProfile( Weapon weapon )
	{
		if ( weapon == null || !weapon.IsValid ) return null;
		if ( !IsPlayer && weapon.BotComboData != null )
			return weapon.BotComboData;

		return weapon.ComboData;
	}

	private Transform? ResolveAttachment( string attachName )
	{
		if ( PreferredRenderer != null && PreferredRenderer.IsValid )
		{
			var attach = PreferredRenderer.GetAttachment( attachName );
			if ( attach.HasValue ) return attach;
		}

		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( !rend.IsValid ) continue;

			var attach = rend.GetAttachment( attachName );
			if ( attach.HasValue ) return attach;
		}

		return null;
	}

	private Vector3 ResolveOrigin( AttackProfile profile, Vector3 eyePos, Rotation aimRotation )
	{
		if ( !string.IsNullOrWhiteSpace( profile.SocketAttachment ) )
		{
			for ( int i = 0; i < _cachedRenderers.Count; i++ )
			{
				var rend = _cachedRenderers[i];
				if ( !rend.IsValid ) continue;

				var attach = rend.GetAttachment( profile.SocketAttachment );
				if ( attach.HasValue )
					return attach.Value.Position;
			}
		}

		return eyePos + (aimRotation * profile.OriginOffset);
	}

	public void SetParry( bool active )
	{
		if ( _fighter != null && !_fighter.IsDead )
			_fighter.SetParry( active );
	}

	public void EquipWeapon( Weapon weapon )
	{
		if ( IsExecutingThrow || IsThrowAnticipating )
		{
			_throwActionToken++;
			IsExecutingThrow = false;
			IsThrowAnticipating = false;
			BroadcastThrowReset();
		}

		if ( weapon.Type == WeaponType.Shield )
		{
			if ( ShieldItem != null ) DropShield();
			ShieldItem = weapon;
		}
		else
		{
			if ( MainWeapon != null ) DropMainWeapon();
			MainWeapon = weapon;
		}

		RefreshRenderersCache();

		var parentObj = PreferredRenderer?.GameObject ?? GameObject;
		weapon.EquipTo( _fighter, parentObj );

		_currentComboIndex = 0;
		UpdateAnimWeaponType();
		SyncWeaponsToAttachments();
	}

	public void DropMainWeapon( Vector3? customVelocity = null )
	{
		CancelAnticipation();
		if ( MainWeapon == null || !MainWeapon.IsValid ) return;

		var attach = ResolveAttachment( WeaponAttachmentName );
		Vector3 dropPos;
		Rotation dropRot;

		if ( attach.HasValue )
		{
			dropPos = attach.Value.Position;
			dropRot = attach.Value.Rotation;
		}
		else if ( _fighter != null && _fighter.IsValid )
		{
			dropPos = _fighter.WorldPosition + Vector3.Up * 45f + WorldRotation.Forward * 20f;
			dropRot = WorldRotation;
		}
		else
		{
			dropPos = MainWeapon.WorldPosition;
			dropRot = WorldRotation;
		}

		var vel = customVelocity ?? (WorldRotation.Forward * 140f + Vector3.Up * 80f);
		MainWeapon.Drop( vel, isLethalThrow: false, dropPosition: dropPos, dropRotation: dropRot );
		MainWeapon = null;

		_currentComboIndex = 0;
		UpdateAnimWeaponType();
	}

	public void DropShield( Vector3? customVelocity = null )
	{
		CancelAnticipation();
		if ( ShieldItem == null || !ShieldItem.IsValid ) return;

		string socket = (MainWeapon == null) ? WeaponAttachmentName : ShieldAttachmentName;
		var attach = ResolveAttachment( socket );
		Vector3 dropPos;
		Rotation dropRot;

		if ( attach.HasValue )
		{
			dropPos = attach.Value.Position;
			dropRot = attach.Value.Rotation;
		}
		else if ( _fighter != null && _fighter.IsValid )
		{
			dropPos = _fighter.WorldPosition + Vector3.Up * 45f - WorldRotation.Right * 20f;
			dropRot = WorldRotation;
		}
		else
		{
			dropPos = ShieldItem.WorldPosition;
			dropRot = WorldRotation;
		}

		var vel = customVelocity ?? (-WorldRotation.Right * 80f + Vector3.Up * 70f);
		ShieldItem.Drop( vel, isLethalThrow: false, dropPosition: dropPos, dropRotation: dropRot );
		ShieldItem = null;

		_currentComboIndex = 0;
		UpdateAnimWeaponType();
	}

	public void DropAllWeapons()
	{
		CancelAnticipation();
		DropMainWeapon( Vector3.Down * 30f );
		DropShield( Vector3.Down * 30f );
	}

	public void UpdateAnimWeaponType( WeaponType? explicitType = null )
	{
		WeaponType activeType = WeaponType.Unarmed;

		if ( explicitType.HasValue )
		{
			activeType = explicitType.Value;
		}
		else if ( MainWeapon != null && MainWeapon.IsValid )
		{
			activeType = MainWeapon.Type;
		}
		else if ( ShieldItem != null && ShieldItem.IsValid )
		{
			activeType = ShieldItem.Type;
		}

		bool hasShield = ShieldItem != null && ShieldItem.IsValid;

		for ( int i = 0; i < _cachedRenderers.Count; i++ )
		{
			var rend = _cachedRenderers[i];
			if ( rend.IsValid )
			{
				rend.Set( "weapon_type", (int)activeType );
				rend.Set( "has_shield", hasShield );
			}
		}
	}
}