combat/weapon.cs
using System;
using Sandbox;

public enum WeaponPhysicsState
{
    Equipped,
    Ground,
    AirborneFloating,
    LethalProjectile,
    Impaled
}

public sealed class Weapon : Component
{
    [Property, Group( "1. Données" )]
    public WeaponComboResource ComboData { get; set; }

    [Property, Group( "1. Données" )]
    [Description( "Profil de combat optionnel réservé aux bots IA (animations 3PP et équilibrage dédiés)." )]
    public WeaponComboResource BotComboData { get; set; }

    [Property, Group( "2. Physique" )] public Collider MainCollider { get; set; }
    [Property, Group( "2. Physique" )] public Rigidbody MainRigidbody { get; set; }

    [Property, Group( "3. Shady Knight (Kick & Pop)" )]
    public float PopUpwardForce { get; set; } = 220f;

    [Property, Group( "3. Shady Knight (Kick & Pop)" )]
    public float PopForwardForce { get; set; } = 30f;

    [Property, Group( "3. Shady Knight (Kick & Pop)" )]
    public float AirborneGravityScale { get; set; } = 0.35f;

    [Property, Group( "3. Shady Knight (Kick & Pop)" )]
    public Vector3 PopSpinVelocity { get; set; } = new( 2f, 16f, 4f );

    [Property, Group( "3. Shady Knight (Kick & Pop)" )]
    public float AerialKickSpeed { get; set; } = 1400f;

    [Property, Group( "4. Règles d'Empalement" )]
    public bool CanImpaleOnThrow { get; set; } = true;

    [Property, Group( "4. Règles d'Empalement" )]
    public bool CanImpaleOnAerialKick { get; set; } = false;

    public WeaponType Type => ComboData?.Type ?? WeaponType.Glaive;
    public string WeaponName => ComboData?.WeaponName ?? "Arme inconnue";
    public bool IsEquipped => CurrentState == WeaponPhysicsState.Equipped;
    public Fighter OwnerFighter { get; private set; }
    public Fighter ImpaledVictim { get; private set; }
    public WeaponPhysicsState CurrentState { get; private set; } = WeaponPhysicsState.Ground;

    private Vector3 _lastWorldPos;
    private TimeSince _timeSinceStateChanged = 0f;
    private Fighter _lastThrower;
    private bool _isAerialKickProjectile = false;

    private SkinnedModelRenderer _impaledRenderer;
    private string _impaledAttachmentName;
    private Transform _impaledLocalOffset;

    protected override void OnAwake()
    {
        MainCollider ??= Components.Get<Collider>();
        MainRigidbody ??= Components.Get<Rigidbody>();
        _lastWorldPos = WorldPosition;
    }

    public void EquipTo( Fighter newOwner, GameObject socket )
    {
        OwnerFighter = newOwner;
        _lastThrower = newOwner;
        CurrentState = WeaponPhysicsState.Equipped;
        _isAerialKickProjectile = false;
        ClearImpaleData();

        if ( MainRigidbody != null )
        {
            MainRigidbody.MotionEnabled = false;
            MainRigidbody.Velocity = Vector3.Zero;
            MainRigidbody.AngularVelocity = Vector3.Zero;
        }

        if ( MainCollider != null )
            MainCollider.Enabled = false;

        GameObject.Parent = socket;
        GameObject.LocalPosition = Vector3.Zero;
        GameObject.LocalRotation = Rotation.Identity;

        GameObject.Tags.Remove( "weapon_drop" );
    }

    public void Drop( Vector3 throwVelocity, bool isLethalThrow = false, Vector3? dropPosition = null, Rotation? dropRotation = null )
    {
        Vector3 targetPos = dropPosition ?? WorldPosition;
        Rotation targetRot = dropRotation ?? WorldRotation;

        if ( targetPos.LengthSquared < 1f && OwnerFighter != null && OwnerFighter.IsValid )
        {
            targetPos = OwnerFighter.WorldPosition + Vector3.Up * 45f;
            targetRot = OwnerFighter.WorldRotation;
        }

        _lastThrower = OwnerFighter;
        OwnerFighter = null;

        GameObject.Parent = null;
        WorldPosition = targetPos;
        WorldRotation = targetRot;

        _isAerialKickProjectile = false;
        ClearImpaleData();

        if ( MainCollider != null )
            MainCollider.Enabled = true;

        if ( MainRigidbody != null )
        {
            MainRigidbody.MotionEnabled = true;
            MainRigidbody.Velocity = throwVelocity;
            MainRigidbody.AngularVelocity = isLethalThrow 
                ? Vector3.Zero 
                : new Vector3( Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ) );
        }

        GameObject.Tags.Add( "weapon_drop" );
        _timeSinceStateChanged = 0f;
        _lastWorldPos = WorldPosition;

        if ( isLethalThrow )
        {
            CurrentState = WeaponPhysicsState.LethalProjectile;
            if ( throwVelocity.Length > 10f )
                WorldRotation = Rotation.LookAt( throwVelocity.Normal );
        }
        else
        {
            CurrentState = WeaponPhysicsState.Ground;
        }
    }

    public void Dislodge()
    {
        if ( CurrentState != WeaponPhysicsState.Impaled ) return;

        var dropPos = WorldPosition;
        var dropRot = WorldRotation;
        ClearImpaleData();

        var dropVel = new Vector3( Game.Random.Float( -15f, 15f ), Game.Random.Float( -15f, 15f ), -45f );
        Drop( dropVel, isLethalThrow: false, dropPosition: dropPos, dropRotation: dropRot );

        Log.Info( $"💥 [{GameObject.Name}] Décrochée du corps !" );
    }

    public void ReceiveKick( Vector3 kickOrigin, Vector3 kickForward, Fighter kicker )
    {
        if ( IsEquipped || CurrentState == WeaponPhysicsState.Impaled ) return;

        _lastThrower = kicker;

        if ( CurrentState == WeaponPhysicsState.AirborneFloating || 
             (CurrentState == WeaponPhysicsState.Ground && WorldPosition.z > kickOrigin.z - 30f && (MainRigidbody?.Velocity.Length ?? 0f) > 75f) )
        {
            CurrentState = WeaponPhysicsState.LethalProjectile;
            _isAerialKickProjectile = true;
            _timeSinceStateChanged = 0f;

            var launchDir = (kickForward.WithZ( 0 ).Normal + Vector3.Up * 0.04f).Normal;

            if ( MainRigidbody != null )
            {
                MainRigidbody.MotionEnabled = true;
                MainRigidbody.Velocity = launchDir * AerialKickSpeed;
                MainRigidbody.AngularVelocity = Vector3.Zero;
            }

            WorldRotation = Rotation.LookAt( launchDir );
            Log.Info( $"⚡ [{GameObject.Name}] KICK AÉRIEN ! Vitesse: {AerialKickSpeed} u/s" );
            return;
        }

        CurrentState = WeaponPhysicsState.AirborneFloating;
        _isAerialKickProjectile = false;
        _timeSinceStateChanged = 0f;

        if ( MainRigidbody != null )
        {
            MainRigidbody.MotionEnabled = true;
            MainRigidbody.Velocity = Vector3.Up * PopUpwardForce + kickForward.WithZ( 0 ).Normal * PopForwardForce;
            MainRigidbody.AngularVelocity = PopSpinVelocity;
        }

        Log.Info( $"🎈 [{GameObject.Name}] POP SHADY KNIGHT !" );
    }

    protected override void OnFixedUpdate()
    {
        switch ( CurrentState )
        {
            case WeaponPhysicsState.AirborneFloating:
                UpdateAirborneFloat();
                break;

            case WeaponPhysicsState.LethalProjectile:
                UpdateLethalFlight();
                break;

            case WeaponPhysicsState.Impaled:
                if ( ImpaledVictim == null || !ImpaledVictim.IsValid || ImpaledVictim.Status == FighterStatus.Active )
                {
                    Dislodge();
                }
                break;
        }

        _lastWorldPos = WorldPosition;
    }

    protected override void OnPreRender()
    {
        if ( CurrentState == WeaponPhysicsState.Impaled )
            UpdateImpaledPosition();
    }

    private void UpdateAirborneFloat()
    {
        if ( MainRigidbody != null && MainRigidbody.MotionEnabled )
        {
            MainRigidbody.Velocity += Scene.PhysicsWorld.Gravity * (AirborneGravityScale - 1.0f) * Time.Delta;
        }

        if ( _timeSinceStateChanged > 2.5f || ((MainRigidbody?.Velocity.Length ?? 0f) < 20f && _timeSinceStateChanged > 0.8f) )
        {
            CurrentState = WeaponPhysicsState.Ground;
        }
    }

    private void UpdateLethalFlight()
    {
        var currentVel = MainRigidbody?.Velocity ?? Vector3.Zero;
        if ( currentVel.LengthSquared > 2500f )
        {
            WorldRotation = Rotation.Slerp( WorldRotation, Rotation.LookAt( currentVel.Normal ), Time.Delta * 20f );
        }

        var moveDelta = WorldPosition - _lastWorldPos;
        var traceDist = moveDelta.Length;

        if ( traceDist > 0.01f )
        {
            var trace = Scene.Trace.Ray( _lastWorldPos, WorldPosition + moveDelta.Normal * 15f )
                .Radius( 6f )
                .UseHitboxes()
                .IgnoreGameObjectHierarchy( GameObject );

            if ( _lastThrower != null && _lastThrower.IsValid )
                trace = trace.IgnoreGameObjectHierarchy( _lastThrower.GameObject );

            var result = trace.Run();

            if ( result.Hit )
            {
                OnProjectileHit( result, currentVel.Normal );
            }
        }

        if ( _timeSinceStateChanged > 4.0f || currentVel.Length < 120f )
        {
            CurrentState = WeaponPhysicsState.Ground;
        }
    }

    private void OnProjectileHit( SceneTraceResult trace, Vector3 hitDir )
    {
        var victim = trace.GameObject?.Components.Get<Fighter>( FindMode.EverythingInSelfAndAncestors )
                  ?? trace.GameObject?.Components.GetInParent<Fighter>();

        if ( victim != null && victim != _lastThrower && !victim.IsDead )
        {
            var victimRenderer = trace.GameObject?.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants )
                              ?? victim.Components.GetInChildren<SkinnedModelRenderer>();

            var boneName = (victimRenderer?.Model != null && trace.Bone >= 0) ? victimRenderer.Model.GetBoneName( trace.Bone ) : "Unknown";
            var profile = ComboData?.ThrownProfile ?? new AttackProfile();

            var hitZone = CombatGeometry.ResolveHitZone( boneName );
            var hitDirection = CombatGeometry.CalculateHitDirection( victim.WorldPosition, victim.WorldRotation, _lastWorldPos );

            bool canImpaleByMode = _isAerialKickProjectile ? CanImpaleOnAerialKick : CanImpaleOnThrow;
            bool isImpaleType = profile.Type == DamageType.Pierce || profile.Type == DamageType.Slash;
            bool willImpale = canImpaleByMode && isImpaleType;

            var context = new DamageContext
            {
                Attacker = _lastThrower,
                HealthDamage = profile.Damage,
                BalanceDamage = profile.BalanceDamage,
                KnockbackForce = hitDir * profile.FighterKnockbackForce,
                HitPosition = trace.HitPosition,
                Zone = hitZone,
                Direction = hitDirection,
                Type = profile.Type,
                IsHeavy = profile.IsHeavy,
                CausesKnockdown = true,
                ForceKnockdown = willImpale
            };

            // Émission vers CombatEvents avec le statut d'empalement précis
            bool throwerIsPlayer = _lastThrower == null || _lastThrower.Components.Get<PlayerController>() != null;
            if ( throwerIsPlayer )
            {
                CombatEvents.EmitPlayerWeaponThrowHit( this, victim, context, willImpale );
            }

            victim.ApplyDamage( context );

            if ( willImpale )
            {
                var impaleSound = ComboData?.ImpaleSound ?? ComboData?.HitFleshSound;
                if ( impaleSound != null )
                    Sound.Play( impaleSound, trace.HitPosition );

                AttachToVictimAnatomy( victimRenderer, victim, hitZone, trace.HitPosition, hitDirection );
                return;
            }
            else
            {
                var fleshSound = ComboData?.HitFleshSound;
                if ( fleshSound != null )
                    Sound.Play( fleshSound, trace.HitPosition );
            }
        }
        else
        {
            var worldSound = ComboData?.HitWorldSound;
            if ( worldSound != null )
                Sound.Play( worldSound, trace.HitPosition );
        }

        CurrentState = WeaponPhysicsState.Ground;
        _isAerialKickProjectile = false;

        if ( MainRigidbody != null )
        {
            var reflectVel = Vector3.Reflect( MainRigidbody.Velocity, trace.Normal ) * 0.35f;
            MainRigidbody.Velocity = reflectVel + Vector3.Up * 80f;
        }
    }

    private void AttachToVictimAnatomy( SkinnedModelRenderer renderer, Fighter victim, HitZone zone, Vector3 hitPos, HitDirection hitDir )
    {
        CurrentState = WeaponPhysicsState.Impaled;
        _timeSinceStateChanged = 0f;

        if ( MainRigidbody != null )
        {
            MainRigidbody.MotionEnabled = false;
            MainRigidbody.Velocity = Vector3.Zero;
            MainRigidbody.AngularVelocity = Vector3.Zero;
        }

        if ( MainCollider != null )
            MainCollider.Enabled = false;

        _impaledRenderer = renderer;
        ImpaledVictim = victim;

        bool isFromBack = (hitDir == HitDirection.Back);

        string targetAttachName = zone switch
        {
            HitZone.Head => "impale_head",
            HitZone.Torso => "impale_chest",
            HitZone.Legs => "impale_leg",
            _ => "impale_chest"
        };

        if ( renderer != null && renderer.IsValid && renderer.GetAttachment( targetAttachName ).HasValue )
        {
            _impaledAttachmentName = targetAttachName;
            var attachTransform = renderer.GetAttachment( targetAttachName ).Value;
            
            WorldPosition = attachTransform.Position;
            WorldRotation = isFromBack ? attachTransform.Rotation * Rotation.FromYaw( 180f ) : attachTransform.Rotation;
            
            Log.Info( $"📌 [{GameObject.Name}] EMPALÉ sur '{targetAttachName}' (Dos: {isFromBack})" );
            return;
        }

        _impaledAttachmentName = null;
        var victimForward = victim.WorldRotation.Forward.WithZ( 0 ).Normal;
        var cleanDirection = isFromBack ? victimForward : -victimForward;
        var cleanRotation = Rotation.LookAt( cleanDirection, Vector3.Up );

        if ( renderer != null && renderer.IsValid )
        {
            _impaledLocalOffset = renderer.WorldTransform.ToLocal( new Transform( hitPos, cleanRotation ) );
            GameObject.Parent = renderer.GameObject;
        }
        else
        {
            WorldPosition = hitPos;
            WorldRotation = cleanRotation;
        }

        Log.Info( $"📌 [{GameObject.Name}] EMPALÉ verrouillé (Dos: {isFromBack})" );
    }

    private void UpdateImpaledPosition()
    {
        if ( _impaledRenderer == null || !_impaledRenderer.IsValid )
        {
            Dislodge();
            return;
        }

        if ( !string.IsNullOrWhiteSpace( _impaledAttachmentName ) )
        {
            var attach = _impaledRenderer.GetAttachment( _impaledAttachmentName );
            if ( attach.HasValue )
            {
                WorldPosition = attach.Value.Position;
                return;
            }
        }

        var worldTrans = _impaledRenderer.WorldTransform.ToWorld( _impaledLocalOffset );
        WorldPosition = worldTrans.Position;
        WorldRotation = worldTrans.Rotation;
    }

    private void ClearImpaleData()
    {
        _impaledRenderer = null;
        _impaledAttachmentName = null;
        ImpaledVictim = null;
    }
}