ai/states/ai_engage_state.cs
// AiEngageState.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;

public sealed class AiEngageState : AiState
{
    public override string StateName => "Engage";

    private enum CombatTactic
    {
        Approaching,
        Circling,
        Attacking,
        Retreating
    }

    private CombatTactic _currentTactic = CombatTactic.Approaching;
    private TimeSince _tacticTimer = 0f;
    private TimeSince _attackCooldownTimer = 0f;
    private bool _strafeRight = true;
    private bool _isExecutingComboRoutine = false;

    private float _combatWeight = 0f;
    private readonly List<SkinnedModelRenderer> _renderers = new();

    public override void OnEnter()
    {
        _currentTactic = CombatTactic.Approaching;
        _tacticTimer = 0f;
        _attackCooldownTimer = 0.8f;
        _isExecutingComboRoutine = false;

        _renderers.Clear();
        _renderers.AddRange( Brain.Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ) );

        Brain.Combat?.UpdateAnimWeaponType();
    }

    public override void OnUpdate()
    {
        if ( !Blackboard.HasValidTarget ) return;

        _combatWeight = MathX.Lerp( _combatWeight, 1.0f, Time.Delta * 8f );
        ApplyStance( true, _combatWeight );

        if ( Combat.CurrentWeapon == null && Blackboard.NearestWeaponDrop != null && Blackboard.DistanceToTarget > 180f )
        {
            Brain.TransitionTo<AiSeekWeaponState>();
            return;
        }

        var dist = Blackboard.DistanceToTarget;
        var attackRange = MathF.Max( GetEffectiveAttackRange(), 85f );
        var circleDistance = attackRange + 65f;

        // Orientation : course face à la cible si loin, sinon face-à-face
        if ( _currentTactic == CombatTactic.Approaching && dist > circleDistance && Brain.Agent.Velocity.LengthSquared > 400f )
        {
            var moveRot = Rotation.LookAt( Brain.Agent.Velocity.WithZ( 0 ).Normal, Vector3.Up );
            Brain.WorldRotation = Rotation.Slerp( Brain.WorldRotation, moveRot, Time.Delta * 8f );
        }
        else
        {
            var lookRotation = Rotation.LookAt( Blackboard.DirectionToTarget.WithZ( 0 ), Vector3.Up );
            Brain.WorldRotation = Rotation.Slerp( Brain.WorldRotation, lookRotation, Time.Delta * 10f );
        }

        var targetHead = Blackboard.Target.WorldPosition + Vector3.Up * 62f;
        SetAimTarget( targetHead );

        if ( !_isExecutingComboRoutine )
            ExecuteTactic( dist, attackRange, circleDistance );
    }

    private void ExecuteTactic( float dist, float attackRange, float circleDistance )
    {
        switch ( _currentTactic )
        {
            case CombatTactic.Approaching:
                Brain.Agent.MoveTo( Blackboard.Target.WorldPosition );
                if ( dist <= circleDistance )
                {
                    _currentTactic = CombatTactic.Circling;
                    _tacticTimer = 0f;
                }
                break;

            case CombatTactic.Circling:
                if ( _tacticTimer > 1.6f )
                {
                    _strafeRight = Game.Random.Float() > 0.5f;
                    _tacticTimer = 0f;
                }

                var lateral = _strafeRight ? Brain.WorldRotation.Right : Brain.WorldRotation.Left;
                var strafeDest = Blackboard.Target.WorldPosition + (lateral * 75f) - (Brain.WorldRotation.Forward * 20f);
                Brain.Agent.MoveTo( strafeDest );

                if ( _tacticTimer > 0.8f && _attackCooldownTimer > 1.6f )
                {
                    _currentTactic = CombatTactic.Attacking;
                    _tacticTimer = 0f;
                }
                break;

            case CombatTactic.Attacking:
                Brain.Agent.MoveTo( Blackboard.Target.WorldPosition );

                bool inRange = dist <= attackRange;
                bool isBlocked = _tacticTimer > 0.35f && Brain.Agent.Velocity.Length < 25f && dist <= (attackRange * 1.25f);

                if ( inRange || isBlocked )
                {
                    Brain.Agent.Stop();
                    DecideAndExecuteStrike();

                    _attackCooldownTimer = 0f;
                    _currentTactic = CombatTactic.Retreating;
                    _tacticTimer = 0f;
                }
                else if ( _tacticTimer > 2.2f )
                {
                    _currentTactic = CombatTactic.Circling;
                    _tacticTimer = 0f;
                }
                break;

            case CombatTactic.Retreating:
                var retreatPos = Brain.WorldPosition - (Brain.WorldRotation.Forward * 85f);
                Brain.Agent.MoveTo( retreatPos );

                if ( _tacticTimer > 0.7f || dist >= circleDistance )
                {
                    _currentTactic = CombatTactic.Circling;
                    _tacticTimer = 0f;
                }
                break;
        }
    }

    private void DecideAndExecuteStrike()
    {
        var targetFighter = Blackboard.TargetFighter;
        var eyePos = Brain.WorldPosition + Vector3.Up * 60f;
        var forward = Brain.WorldRotation.Forward;
        var combo = Combat.ActiveProfile?.PrimaryCombo;

        bool targetVulnerableToKick = targetFighter != null && (targetFighter.IsParrying || targetFighter.BalanceRatio < 0.30f);
        if ( targetVulnerableToKick && Game.Random.Float() < 0.70f )
        {
            Combat.Kick( eyePos, forward );
            return;
        }

        bool canDoDoubleCombo = combo != null && combo.Count >= 3;
        bool targetStaggered = targetFighter != null && targetFighter.Status == FighterStatus.Staggered;

        if ( canDoDoubleCombo && (targetStaggered || Game.Random.Float() < 0.40f) )
        {
            _ = ExecuteLinkedComboRoutineAsync( combo[1], combo[2], eyePos, forward );
            return;
        }

        if ( combo != null && combo.Count > 0 )
        {
            Combat.ExecuteSpecificAttack( combo[0], eyePos, forward );
        }
        else
        {
            Combat.Attack( eyePos, forward );
        }
    }

    private async Task ExecuteLinkedComboRoutineAsync( AttackProfile hit1, AttackProfile hit2, Vector3 eyePos, Vector3 forward )
    {
        _isExecutingComboRoutine = true;
        Brain.Agent?.Stop();

        Combat.ExecuteSpecificAttack( hit1, eyePos, forward );

        if ( hit1.Cooldown > 0f )
            await Task.Delay( (int)(hit1.Cooldown * 1000f) );

        if ( !Brain.IsValid || Fighter == null || Fighter.Status != FighterStatus.Active )
        {
            _isExecutingComboRoutine = false;
            return;
        }

        var updatedEyePos = Brain.WorldPosition + Vector3.Up * 60f;
        var updatedForward = Blackboard.HasValidTarget ? Blackboard.DirectionToTarget : Brain.WorldRotation.Forward;

        Combat.ExecuteSpecificAttack( hit2, updatedEyePos, updatedForward );

        _isExecutingComboRoutine = false;
    }

    private float GetEffectiveAttackRange()
    {
        var profile = Combat.ActiveProfile?.PrimaryCombo;
        if ( profile != null && profile.Count > 0 )
            return profile[0].Range;

        return 80f;
    }

    public override void OnExit()
    {
        Brain.Agent?.Stop();
        _isExecutingComboRoutine = false;
        _combatWeight = 0f;
        ApplyStance( false, 0f );
    }

    private void ApplyStance( bool inCombat, float weight )
    {
        var currentWeaponType = (int)(Brain.Combat?.CurrentWeapon?.Type ?? WeaponType.Unarmed);

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

            rend.Set( "in_combat", inCombat );
            rend.Set( "in_combat_weight", weight );
            rend.Set( "look_weight", weight );
            rend.Set( "weapon_type", currentWeaponType );
        }
    }

    private void SetAimTarget( Vector3 targetPos )
    {
        for ( int i = 0; i < _renderers.Count; i++ )
        {
            var rend = _renderers[i];
            if ( rend.IsValid )
                rend.Set( "aim_target", targetPos );
        }
    }

    public override string GetDebugStatus() => $"{StateName} [{_currentTactic}] Cible: {Blackboard.Target?.Name ?? "Aucune"}";
}