fighter/player_controller.cs
// PlayerController.cs
using System;
using Sandbox;

public sealed class PlayerController : Component
{
    [Property, Group( "Camera & Pivot" )] public GameObject EyePivot { get; set; }
    [Property, Group( "Camera & Pivot" )] public CameraComponent Camera { get; set; }
    [Property, Group( "Camera & Pivot" )] public float LookSensitivity { get; set; } = 0.12f;
    [Property, Group( "Camera & Pivot" )] public string CameraAttachmentName { get; set; } = "camera_view";

    [Property, Group( "Renderers" )] public SkinnedModelRenderer ArmsRenderer1PP { get; set; }
    [Property, Group( "Renderers" )] public SkinnedModelRenderer BodyRenderer3PP { get; set; }

    [Property, Group( "Ramassage (Grab F)" )] public float GrabDistance { get; set; } = 130f;
    [Property, Group( "Ramassage (Grab F)" )] public float GrabConeAngle { get; set; } = 55f;

    [Property, Group( "Jet d'Arme" )] public float MaxThrowChargeTime { get; set; } = 0.45f;
    [Property, Group( "Jet d'Arme" )] public float MinThrowSpeed { get; set; } = 650f;
    [Property, Group( "Jet d'Arme" )] public float MaxThrowSpeed { get; set; } = 1500f;

    [Property, Group( "Input Actions" )] public string GrabAction { get; set; } = "Grab";
    [Property, Group( "Input Actions" )] public string DropAction { get; set; } = "Drop";
    [Property, Group( "Input Actions" )] public string ThrowAction { get; set; } = "Throw";
    [Property, Group( "Input Actions" )] public string ThrowAltAction { get; set; } = "ThrowAlt";

    private FighterMovement _movement;
    private FighterCombat _combat;
    private Angles _eyeAngles;

    private Vector3? _restPosInPivot;
    private Rotation? _restRotInPivot;
    private TimeSince _timeSinceStart = 0f;
    private bool _isHoldingKick = false;

    protected override void OnAwake()
    {
        _movement = Components.Get<FighterMovement>();
        _combat = Components.Get<FighterCombat>();
        Camera ??= Components.GetInChildren<CameraComponent>();

        if ( _combat != null && ArmsRenderer1PP != null )
        {
            _combat.PreferredRenderer = ArmsRenderer1PP;
        }
    }

    protected override void OnStart()
    {
        if ( EyePivot != null )
            _eyeAngles = EyePivot.WorldRotation.Angles();

        _timeSinceStart = 0f;
    }

    protected override void OnUpdate()
    {
        if ( UIStateManager.IsGameplayLocked )
        {
            if ( _movement != null )
            {
                _movement.WishVelocity = Vector3.Zero;
                _movement.WantsSprint = false;
                _movement.WantsCrouch = false;
            }
            _isHoldingKick = false;
            _combat?.CancelAnticipation();
            return;
        }

        // 1. Rotation de la vue
        var look = Input.AnalogLook * LookSensitivity;
        _eyeAngles.pitch = Math.Clamp( _eyeAngles.pitch + look.pitch, -85f, 85f );
        _eyeAngles.yaw += look.yaw;

        WorldRotation = Rotation.FromYaw( _eyeAngles.yaw );

        if ( EyePivot != null )
        {
            EyePivot.LocalRotation = Rotation.FromPitch( _eyeAngles.pitch );
        }

        // 2. Déplacement physique
        var moveInput = Input.AnalogMove;
        var forward = WorldRotation.Forward * moveInput.x;
        var right = WorldRotation.Right * -moveInput.y;
        _movement.WishVelocity = (forward + right).Normal;

        _movement.WantsSprint = Input.Down( "run" );
        _movement.WantsCrouch = Input.Down( "duck" );

        if ( Input.Pressed( "jump" ) )
            _movement.Jump();

        var eyePos = EyePivot != null ? EyePivot.WorldPosition : WorldPosition + Vector3.Up * 64f;
        var aimDir = EyePivot != null ? EyePivot.WorldRotation.Forward : WorldRotation.Forward;

        // 3. COMBAT : Attaque Principale
        if ( Input.Pressed( "attack1" ) && !_isHoldingKick && !_combat.IsThrowAnticipating )
        {
            _combat.StartPrimaryAttack( eyePos, aimDir );
        }
        else if ( Input.Released( "attack1" ) && !_isHoldingKick && !_combat.IsThrowAnticipating )
        {
            _combat.ReleaseAction( eyePos, aimDir );
        }

        // 4. COMBAT : Spartan Kick sur 'use'
        if ( Input.Pressed( "use" ) && !_combat.IsAnticipating )
        {
            _isHoldingKick = true;
            _combat.StartKick( eyePos, aimDir );
        }
        else if ( Input.Released( "use" ) && _isHoldingKick )
        {
            _isHoldingKick = false;
            _combat.ReleaseAction( eyePos, aimDir );
        }

        // 5. COMBAT : Jet d'arme
        UpdateThrowInput( aimDir );

        // 6. Parade & Feinte
        bool wantsParry = Input.Down( "attack2" );
        if ( wantsParry && _combat.IsAnticipating )
        {
            _isHoldingKick = false;
            _combat.CancelAnticipation();
        }
        _combat.SetParry( wantsParry );

        // 7. Ramassage d'arme sur 'F'
        if ( Input.Pressed( GrabAction ) )
            TryGrabWeapon( eyePos, aimDir );

        // 8. Lâcher l'arme (Drop)
        if ( Input.Pressed( DropAction ) )
        {
            _isHoldingKick = false;
            if ( _combat.MainWeapon != null )
                _combat.DropMainWeapon();
            else if ( _combat.ShieldItem != null )
                _combat.DropShield();
        }

        var inCombat = wantsParry || _combat.CurrentWeapon != null || _combat.IsAnticipating || _combat.IsExecutingThrow;
        UpdateRenderers( inCombat, aimDir );
    }

    private void TryGrabWeapon( Vector3 eyePos, Vector3 aimDir )
    {
        var trace = Scene.Trace.Sphere( 24f, eyePos, eyePos + aimDir * GrabDistance )
            .WithTag( "weapon_drop" )
            .IgnoreGameObjectHierarchy( GameObject )
            .Run();

        var targetedWeapon = trace.GameObject?.Components.Get<Weapon>( FindMode.EverythingInSelfAndAncestors );

        if ( targetedWeapon == null )
        {
            var cosHalf = MathF.Cos( MathX.DegreeToRadian( GrabConeAngle * 0.5f ) );
            targetedWeapon = SpatialQuery.GetNearest<Weapon>( eyePos, GrabDistance, w =>
            {
                if ( w.IsEquipped || !w.GameObject.IsValid ) return false;
                var dirToW = (w.WorldPosition - eyePos).Normal;
                return Vector3.Dot( aimDir, dirToW ) >= cosHalf;
            } );
        }

        if ( targetedWeapon != null && !targetedWeapon.IsEquipped )
        {
            _combat.EquipWeapon( targetedWeapon );
            Log.Info( $"✋ [GRAB] Arme équipée : {targetedWeapon.WeaponName}" );
        }
    }

    private void UpdateThrowInput( Vector3 aimDir )
    {
        bool throwPressed = Input.Pressed( ThrowAction ) || Input.Pressed( ThrowAltAction );
        bool throwReleased = Input.Released( ThrowAction ) || Input.Released( ThrowAltAction );

        if ( throwPressed && _combat.CurrentWeapon != null )
        {
            _combat.StartThrow();
        }
        else if ( throwReleased && _combat.IsThrowAnticipating )
        {
            var chargeRatio = Math.Clamp( _combat.CurrentChargeTime / MaxThrowChargeTime, 0f, 1f );
            var speed = MathX.Lerp( MinThrowSpeed, MaxThrowSpeed, chargeRatio );

            _combat.ReleaseThrow( aimDir, speed );
        }
    }

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

    private void UpdateRenderers( bool inCombat, Vector3 aimDir )
    {
        if ( BodyRenderer3PP != null )
        {
            BodyRenderer3PP.Set( "in_combat", inCombat );
            BodyRenderer3PP.Set( "in_combat_weight", inCombat ? 1.0f : 0.0f );
            BodyRenderer3PP.Set( "aim_pitch", _eyeAngles.pitch );
            BodyRenderer3PP.Set( "aim_target", WorldPosition + aimDir * 120f + Vector3.Up * 60f );
        }

        if ( ArmsRenderer1PP != null )
        {
            ArmsRenderer1PP.Set( "in_combat", inCombat );
            ArmsRenderer1PP.Set( "is_parrying", Input.Down( "attack2" ) );
        }
    }

    private void ApplyCameraBoneMotion()
    {
        if ( Camera == null || ArmsRenderer1PP == null || EyePivot == null ) return;

        var attach = ArmsRenderer1PP.GetAttachment( CameraAttachmentName );
        if ( !attach.HasValue )
        {
            Camera.LocalPosition = Vector3.Zero;
            Camera.LocalRotation = Rotation.Identity;
            return;
        }

        var currentPosInPivot = EyePivot.WorldTransform.PointToLocal( attach.Value.Position );
        var currentRotInPivot = EyePivot.WorldRotation.Inverse * attach.Value.Rotation;

        if ( !_restPosInPivot.HasValue && _timeSinceStart > 0.15f )
        {
            _restPosInPivot = currentPosInPivot;
            _restRotInPivot = currentRotInPivot;
        }

        if ( !_restPosInPivot.HasValue ) return;

        var deltaPos = currentPosInPivot - _restPosInPivot.Value;
        var deltaRot = _restRotInPivot.Value.Inverse * currentRotInPivot;

        Camera.LocalPosition = deltaPos;
        Camera.LocalRotation = deltaRot;
    }
}