fighter/fighter.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;

public enum FighterStatus
{
    Active,
    Staggered,
    KnockedDown,
    Dead
}

public sealed class Fighter : Component
{
    [Property, Group( "1. Condition Physique" )] public float MaxHealth { get; set; } = 100f;
    [Property, Group( "1. Condition Physique" )] public float CurrentHealth { get; set; } = 100f;
    [Property, Group( "1. Condition Physique" )] public float HealthRegenRate { get; set; } = 1.5f;
    [Property, Group( "1. Condition Physique" )] public float HealthPalierStep { get; set; } = 25f;

    [Property, Group( "2. Équilibre" )] public float MaxBalance { get; set; } = 100f;
    [Property, Group( "2. Équilibre" )] public float CurrentBalance { get; set; } = 100f;
    [Property, Group( "2. Équilibre" )] public float BaseBalanceRegenRate { get; set; } = 35f;
    [Property, Group( "2. Équilibre" )] public float BalanceRegenDelay { get; set; } = 1.2f;
    [Property, Group( "2. Équilibre" )] public float MinHealthBalanceRegenMult { get; set; } = 0.25f;

    [Property, Group( "3. Adrénaline" )] public float MaxAdrenaline { get; set; } = 100f;
    [Property, Group( "3. Adrénaline" )] public float CurrentAdrenaline { get; set; } = 0f;
    [Property, Group( "3. Adrénaline" )] public float AdrenalineDecayRate { get; set; } = 2.5f;
    [Property, Group( "3. Adrénaline" )] public float AdrenalineDecayDelay { get; set; } = 4.0f;
    [Property, Group( "3. Adrénaline" )] public float LowHealthAdrenalineBonus { get; set; } = 1.5f;
    [Property, Group( "3. Adrénaline" )] public float MaxAdrenalineBalanceBoost { get; set; } = 2.0f;

    [Property, Group( "États & Vulnérabilité" )] public float PerfectParryWindow { get; set; } = 0.20f;
    [Property, Group( "États & Vulnérabilité" )] public float StaggerDuration { get; set; } = 3.0f;
    [Property, Group( "États & Vulnérabilité" )] public float KnockdownLieDuration { get; set; } = 1.4f;
    [Property, Group( "États & Vulnérabilité" )] public float GetUpDuration { get; set; } = 1.1f;

    public bool IsParrying { get; private set; }
    public FighterStatus Status { get; private set; } = FighterStatus.Active;
    public bool IsDead => Status == FighterStatus.Dead;

    public float HealthRatio => Math.Clamp( CurrentHealth / MaxHealth, 0f, 1f );
    public float BalanceRatio => Math.Clamp( CurrentBalance / MaxBalance, 0f, 1f );
    public float AdrenalineRatio => Math.Clamp( CurrentAdrenaline / MaxAdrenaline, 0f, 1f );

    private TimeSince _timeSinceDamageTaken = 10f;
    private TimeSince _timeSinceBalanceDamaged = 10f;
    private TimeSince _timeSinceCombatActivity = 10f;
    private TimeSince _timeSinceParryStarted = 10f;
    private TimeSince _timeSinceStagger = 10f;

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

    protected override void OnAwake()
    {
        _agent = Components.Get<NavMeshAgent>();
        RefreshRenderers();
    }

    protected override void OnStart()
    {
        RefreshRenderers();
    }

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

    protected override void OnUpdate()
    {
        if ( IsDead ) return;

        UpdateStagger();
        UpdateBalanceRegen();
        UpdateHealthRegen();
        UpdateAdrenalineDecay();
    }

    public void ApplyDamage( DamageContext ctx )
    {
        if ( IsDead ) return;

        _timeSinceCombatActivity = 0f;
        var combat = Components.Get<FighterCombat>();

        if ( IsParrying && _timeSinceParryStarted <= PerfectParryWindow )
        {
            CurrentBalance = MaxBalance;
            AddAdrenaline( 20f );

            combat?.NotifyParrySuccess( ctx.HitPosition );

            if ( ctx.Attacker != null && ctx.Attacker.IsValid )
            {
                ctx.Attacker.TakeBalanceDamage( ctx.Attacker.MaxBalance );
                ctx.Attacker.Components.Get<FighterMovement>()?.ApplyKnockback( -ctx.KnockbackForce * 0.5f );
            }
            return;
        }

        if ( IsParrying )
        {
            bool hasShield = combat != null && combat.HasShieldEquipped;

            float healthDmg = hasShield ? 0f : (ctx.HealthDamage * 0.15f);
            float balanceDmg = hasShield ? (ctx.BalanceDamage * 0.15f) : (ctx.BalanceDamage * 0.40f);

            CurrentHealth = MathF.Max( 0f, CurrentHealth - healthDmg );
            TakeBalanceDamage( balanceDmg );
            AddAdrenaline( 5f );

            combat?.NotifyBlockSuccess( ctx.HitPosition, hasShield );
            Components.Get<FighterMovement>()?.ApplyKnockback( ctx.KnockbackForce * (hasShield ? 0.10f : 0.25f) );
            return;
        }

        var finalHealthDmg = ctx.HealthDamage;
        var finalBalanceDmg = ctx.BalanceDamage;

        if ( ctx.Zone == HitZone.Head )
        {
            finalHealthDmg *= 2.0f;
            finalBalanceDmg *= 0.5f;
        }
        else if ( ctx.Zone == HitZone.Legs )
        {
            finalHealthDmg *= 0.5f;
            finalBalanceDmg *= 2.0f;
        }

        CurrentHealth = MathF.Max( 0f, CurrentHealth - finalHealthDmg );
        _timeSinceDamageTaken = 0f;
        AddAdrenaline( finalHealthDmg * 0.6f );

        var balanceBefore = CurrentBalance;
        var remainingBalance = MathF.Max( 0f, CurrentBalance - finalBalanceDmg );
        bool balanceBroken = (balanceBefore > 0f && remainingBalance <= 0f) || CurrentBalance <= 0f;
        CurrentBalance = remainingBalance;
        _timeSinceBalanceDamaged = 0f;

        bool shouldKnockdown = ctx.ForceKnockdown || 
            (ctx.CausesKnockdown && (balanceBroken || Status == FighterStatus.Staggered || CurrentBalance <= 15f));

        if ( ctx.ForceKnockdown )
            CurrentBalance = 0f;

        if ( CurrentHealth <= 0f )
        {
            Die( ctx, diedOnGround: shouldKnockdown || Status == FighterStatus.KnockedDown );
            return;
        }

        if ( shouldKnockdown )
        {
            TriggerKnockdown( ctx );
            return;
        }

        if ( balanceBroken && Status == FighterStatus.Active )
        {
            TriggerStagger();
        }

        Components.Get<FighterMovement>()?.ApplyKnockback( ctx.KnockbackForce );

        if ( Status == FighterStatus.Active )
        {
            TriggerHitReaction( ctx.Zone, ctx.Direction );
        }

        // Notification d'agression reçue (pour le suivi de l'épargne)
        CombatEvents.EmitFighterDamaged( this, ctx );
    }

    private async void TriggerKnockdown( DamageContext ctx )
    {
        bool attackerIsPlayer = ctx.Attacker != null && ctx.Attacker.Components.Get<PlayerController>() != null;
        if ( attackerIsPlayer && ctx.Attacker != this )
        {
            CombatEvents.EmitPlayerKnockdownEnemy( this, ctx );
        }
        CombatEvents.EmitFighterKnockedDown( this, ctx );

        Status = FighterStatus.KnockedDown;
        CurrentBalance = 0f;

        var combat = Components.Get<FighterCombat>();
        combat?.CancelAttackAnticipation();
        combat?.DropAllWeapons();

        if ( _agent != null && _agent.IsValid )
            _agent.Enabled = false;

        int landingMode = ctx.Direction == HitDirection.Back ? 1 : 0;
        var pushDir = landingMode == 1 ? WorldRotation.Forward : -WorldRotation.Forward;
        Components.Get<FighterMovement>()?.ApplyKnockback( pushDir * 380f );

        SendAnimParameter( "knockdown_landing", landingMode );
        SendAnimParameter( "in_knockdown", true );
        SendAnimParameter( "knockdown", true );

        await Task.Delay( 60 );
        if ( !IsValid || IsDead ) return;
        SendAnimParameter( "knockdown", false );

        await Task.Delay( (int)(KnockdownLieDuration * 1000f) );
        if ( !IsValid || IsDead ) return;

        EjectImpaledWeapons();

        SendAnimParameter( "get_up", true );

        await Task.Delay( 60 );
        if ( !IsValid || IsDead ) return;
        SendAnimParameter( "get_up", false );

        await Task.Delay( (int)(GetUpDuration * 1000f) );
        if ( !IsValid || IsDead ) return;

        Status = FighterStatus.Active;
        CurrentBalance = MaxBalance * 0.35f;
        SendAnimParameter( "in_knockdown", false );

        if ( _agent != null && _agent.IsValid )
            _agent.Enabled = true;

        CombatEvents.EmitFighterRecoveredFromStaggerOrKnockdown( this );
    }

    private void EjectImpaledWeapons()
    {
        foreach ( var weapon in Scene.GetAllComponents<Weapon>() )
        {
            if ( weapon.IsValid && weapon.CurrentState == WeaponPhysicsState.Impaled && weapon.ImpaledVictim == this )
            {
                weapon.Dislodge();
            }
        }
    }

    private void Die( DamageContext ctx, bool diedOnGround )
    {
        var combat = Components.Get<FighterCombat>();
        bool victimWasUnarmed = combat == null || (combat.MainWeapon == null && combat.ShieldItem == null);

        bool attackerIsPlayer = ctx.Attacker != null && ctx.Attacker.Components.Get<PlayerController>() != null;
        if ( attackerIsPlayer && ctx.Attacker != this )
        {
            CombatEvents.EmitPlayerKillEnemy( this, ctx, victimWasUnarmed );
        }
        CombatEvents.EmitFighterKilled( this, ctx );

        Status = FighterStatus.Dead;

        combat?.CancelAttackAnticipation();
        combat?.DropAllWeapons();

        if ( _agent != null && _agent.IsValid )
            _agent.Enabled = false;

        SendAnimParameter( "death_type", (int)ctx.Type );
        SendAnimParameter( "death_zone", (int)ctx.Zone );
        SendAnimParameter( "death_heavy", ctx.IsHeavy );
        SendAnimParameter( "death_direction", (int)ctx.Direction );
        SendAnimParameter( "is_dead", true );

        if ( diedOnGround )
        {
            int landingMode = ctx.Direction == HitDirection.Back ? 1 : 0;
            var pushDir = landingMode == 1 ? WorldRotation.Forward : -WorldRotation.Forward;
            Components.Get<FighterMovement>()?.ApplyKnockback( pushDir * 320f );

            SendAnimParameter( "knockdown_landing", landingMode );
            SendAnimParameter( "in_knockdown", true );
            SendAnimParameter( "knockdown", true );
        }
        else
        {
            SendAnimParameter( "in_knockdown", false );
            SendAnimParameter( "death", true );
        }
        
        if ( Components.Get<PlayerController>() != null )
        {
            GameManager.Instance?.HandlePlayerKnockedDownOrKilled( this );
        }
    }

    private async void TriggerHitReaction( HitZone zone, HitDirection direction )
    {
        SendAnimParameter( "hit_zone", (int)zone );
        SendAnimParameter( "hit_direction", (int)direction );
        SendAnimParameter( "hit_flinch", true );

        await Task.Delay( 60 );
        if ( IsValid && !IsDead )
            SendAnimParameter( "hit_flinch", false );
    }

    private void SendAnimParameter( string param, object val )
    {
        for ( int i = 0; i < _cachedRenderers.Count; i++ )
        {
            var rend = _cachedRenderers[i];
            if ( !rend.IsValid ) continue;

            if ( val is bool b ) rend.Set( param, b );
            else if ( val is int num ) rend.Set( param, num );
            else if ( val is float f ) rend.Set( param, f );
        }
    }

    public void TakeBalanceDamage( float amount )
    {
        CurrentBalance = MathF.Max( 0f, CurrentBalance - amount );
        _timeSinceBalanceDamaged = 0f;

        if ( CurrentBalance <= 0f && Status == FighterStatus.Active )
        {
            TriggerStagger();
        }
    }

    public void AddAdrenaline( float baseAmount )
    {
        var lowHealthMult = 1.0f + ((1.0f - HealthRatio) * LowHealthAdrenalineBonus);
        CurrentAdrenaline = Math.Clamp( CurrentAdrenaline + (baseAmount * lowHealthMult), 0f, MaxAdrenaline );
        _timeSinceCombatActivity = 0f;
    }

    public void SetParry( bool active )
    {
        if ( active && !IsParrying )
            _timeSinceParryStarted = 0f;
        IsParrying = active;
    }

    private void UpdateBalanceRegen()
    {
        if ( Status != FighterStatus.Active ) return;
        if ( _timeSinceBalanceDamaged < BalanceRegenDelay ) return;

        var healthEfficiency = MathX.Lerp( MinHealthBalanceRegenMult, 1.0f, HealthRatio );
        var adrenalineBoost = 1.0f + (AdrenalineRatio * MaxAdrenalineBalanceBoost);
        var finalRate = BaseBalanceRegenRate * healthEfficiency * adrenalineBoost;

        CurrentBalance = Math.Clamp( CurrentBalance + (finalRate * Time.Delta), 0f, MaxBalance );
    }

    private void UpdateHealthRegen()
    {
        if ( _timeSinceDamageTaken < 6.0f ) return;

        var maxPalier = MathF.Ceiling( CurrentHealth / HealthPalierStep ) * HealthPalierStep;
        maxPalier = Math.Clamp( maxPalier, 0f, MaxHealth );

        if ( CurrentHealth < maxPalier )
        {
            CurrentHealth = MathF.Min( maxPalier, CurrentHealth + (HealthRegenRate * Time.Delta) );
        }
    }

    private void UpdateAdrenalineDecay()
    {
        if ( _timeSinceCombatActivity < AdrenalineDecayDelay ) return;
        CurrentAdrenaline = MathF.Max( 0f, CurrentAdrenaline - (AdrenalineDecayRate * Time.Delta) );
    }

    private void TriggerStagger()
    {
        Status = FighterStatus.Staggered;
        _timeSinceStagger = 0f;

        Components.Get<FighterCombat>()?.CancelAttackAnticipation();

        SendAnimParameter( "is_staggered", true );
        Log.Info( $"{GameObject.Name} est Staggered (Garde brisée) !" );
    }

    private void UpdateStagger()
    {
        if ( Status != FighterStatus.Staggered ) return;

        if ( _timeSinceStagger >= StaggerDuration )
        {
            Status = FighterStatus.Active;
            CurrentBalance = MaxBalance * 0.35f;
            SendAnimParameter( "is_staggered", false );
            CombatEvents.EmitFighterRecoveredFromStaggerOrKnockdown( this );
        }
    }
}