combat/attack_profile.cs
using System;
using System.Collections.Generic;
using Sandbox;

public enum AttackShape
{
    DirectTrace,
    HorizontalSweep,
    VerticalSweep
}

public sealed class AttackProfile
{
    [Property, Group( "1. Dégâts & Identité" )] public string Name { get; set; } = "Attaque";
    [Property, Group( "1. Dégâts & Identité" )] public float Damage { get; set; } = 15f;
    [Property, Group( "1. Dégâts & Identité" )] public float BalanceDamage { get; set; } = 25f;
    [Property, Group( "1. Dégâts & Identité" )] public DamageType Type { get; set; } = DamageType.Slash;
    [Property, Group( "1. Dégâts & Identité" )] public bool IsHeavy { get; set; } = false;

    [Property, Group( "2. Timing & Portée" )] public float Range { get; set; } = 75f;
    [Property, Group( "2. Timing & Portée" )] public float Radius { get; set; } = 18f;
    [Property, Group( "2. Timing & Portée" )] public float WindupDelay { get; set; } = 0.05f;
    [Property, Group( "2. Timing & Portée" )] public float Cooldown { get; set; } = 0.4f;
    [Property, Group( "2. Timing & Portée" )] public int AnimIndex { get; set; } = 1;
    [Property, Group( "2. Timing & Portée" )] public bool IsKickAnim { get; set; } = false;

    [Property, Group( "3. Animation Corporelle" )]
    public bool AffectWholeBody { get; set; } = false;

    [Property, Group( "3. Animation Corporelle" )]
    public float FullBodyDuration { get; set; } = 0.85f;

    // =========================================================================
    // IMPACT SUR COMBATTANTS (Valeurs modérées, collées au sol)
    // =========================================================================
    [Property, Group( "4. Recul Combattant (Fighter)" )]
    [Description( "Force de recul horizontal appliquée au combattant touché (ex: 30 à 60 pour une épée, 150 pour un kick)" )]
    public float FighterKnockbackForce { get; set; } = 45f;

    [Property, Group( "4. Recul Combattant (Fighter)" )]
    [Description( "Élévation verticale du combattant. Laissez à 0 pour que le coup reste strictement au sol sans faire décoller la cible." )]
    public float FighterUpwardLift { get; set; } = 0.0f;

    [Property, Group( "4. Recul Combattant (Fighter)" )]
    [Description( "Si coché, projette la cible au sol si sa garde est brisée (Staggered)." )]
    public bool CausesKnockdownOnStagger { get; set; } = false;

    // =========================================================================
    // IMPACT SUR PROPS & DÉCORS (Physique lourde indépendante)
    // =========================================================================
    [Property, Group( "5. Propulsion Décors (Props & Rigidbodies)" )]
    [Description( "Impulsion physique brute (en Ns) transmise exclusivement aux objets dynamiques du décor." )]
    public float PropPhysicsImpulse { get; set; } = 18000f;

    [Property, Group( "5. Propulsion Décors (Props & Rigidbodies)" )]
    [Description( "Biais vertical pour faire voler les objets du décor vers le haut." )]
    public float PropUpwardLift { get; set; } = 0.35f;

    [Property, Group( "6. Origine & Tracé" )] public Vector3 OriginOffset { get; set; } = Vector3.Zero;
    [Property, Group( "6. Origine & Tracé" )] public string SocketAttachment { get; set; } = "";

    // =========================================================================
    // EFFETS & FEEDBACK SONORE PROPRE AU MOUVEMENT
    // =========================================================================
    [Property, Group( "7. Effets" )] public SoundEvent SwingSound { get; set; }

    [Property, Group( "7. Effets" )]
    [Description( "Optionnel. Si assigné, remplace le HitFleshSound de la WeaponComboResource (ex: botte lourde pour un kick)." )]
    public SoundEvent HitSoundOverride { get; set; }

    [Property, Group( "7. Effets" )] public GameObject HitParticlePrefab { get; set; }

    [Property, Group( "8. Géométrie" )] public AttackShape Shape { get; set; } = AttackShape.DirectTrace;
    [Property, Group( "8. Géométrie" )] public int RayCount { get; set; } = 5;
    [Property, Group( "8. Géométrie" )] public float SweepAngle { get; set; } = 60f;

    public IEnumerable<Vector3> GenerateRays( Vector3 forwardDir )
    {
        if ( Shape == AttackShape.DirectTrace || RayCount <= 1 )
        {
            yield return forwardDir;
            yield break;
        }

        var rotationAxis = Shape == AttackShape.HorizontalSweep ? Vector3.Up : Vector3.Cross( forwardDir, Vector3.Up ).Normal;
        var halfAngle = SweepAngle * 0.5f;
        var step = SweepAngle / (RayCount - 1);

        for ( int i = 0; i < RayCount; i++ )
        {
            var angle = -halfAngle + (step * i);
            var rot = Rotation.FromAxis( rotationAxis, angle );
            yield return rot * forwardDir;
        }
    }
}