diff --git a/code/swb_base/Weapon.Shoot.cs b/code/swb_base/Weapon.Shoot.cs index 94669ff..0d3ebc6 100644 --- a/code/swb_base/Weapon.Shoot.cs +++ b/code/swb_base/Weapon.Shoot.cs @@ -101,8 +101,18 @@ public virtual void Shoot( ShootInfo shootInfo, bool isPrimary ) // Barrel smoke barrelHeat += 1; - // Recoil - Owner.ApplyEyeAnglesOffset( GetRecoilAngles( shootInfo ) ); + // Recoil / AimPunch + var recoilAngles = GetRecoilAngles( shootInfo ); + if ( shootInfo.UseAimPunch ) + { + // Apply aimpunch (camera recoil with recovery) + Owner.ApplyAimPunch( recoilAngles, shootInfo.AimPunchRecoverySpeed ); + } + else + { + // Apply instant recoil (legacy behavior) + Owner.ApplyEyeAnglesOffset( recoilAngles ); + } // Screenshake if ( shootInfo.ScreenShake is not null ) diff --git a/code/swb_base/structures/ShootInfo.cs b/code/swb_base/structures/ShootInfo.cs index 19b6645..704d43e 100644 --- a/code/swb_base/structures/ShootInfo.cs +++ b/code/swb_base/structures/ShootInfo.cs @@ -67,6 +67,12 @@ public class ShootInfo : Component /// Weapon recoil [Property, Group( "Bullets" )] public float Recoil { get; set; } = 0.1f; + /// Enable aimpunch (camera recoil with recovery) + [Property, Group( "Bullets" )] public bool UseAimPunch { get; set; } = false; + + /// Aimpunch recovery speed (higher = faster recovery) + [Property, Group( "Bullets" )] public float AimPunchRecoverySpeed { get; set; } = 5f; + /// Rate Per Minute, firing speed (higher is faster) [Property, Group( "Bullets" )] public int RPM { get; set; } = 200; diff --git a/code/swb_player/Inventory.cs b/code/swb_player/Inventory.cs index 9bba76d..0c08486 100644 --- a/code/swb_player/Inventory.cs +++ b/code/swb_player/Inventory.cs @@ -56,6 +56,9 @@ public void SetActive( GameObject gameObject ) oldActive.OnCarryStop(); } + // Reset aimpunch when switching weapons + player?.ResetAimPunch(); + if ( gameObject.Components.TryGet( out var newActive, FindMode.EverythingInSelf ) ) { newActive.OnCarryStart(); diff --git a/code/swb_player/PlayerBase.AimPunch.cs b/code/swb_player/PlayerBase.AimPunch.cs new file mode 100644 index 0000000..a28f5a1 --- /dev/null +++ b/code/swb_player/PlayerBase.AimPunch.cs @@ -0,0 +1,72 @@ +namespace SWB.Player; + +public partial class PlayerBase +{ + Angles aimPunchAngle; + float aimPunchRecoverySpeed = 5f; + RealTimeSince timeSinceLastPunch; + + /// + /// Apply aimpunch recoil to the camera. + /// Similar to Flinch but with configurable recovery and shooting-aware delay. + /// + /// The angular offset to apply + /// How fast the camera recovers (higher = faster) + public virtual void ApplyAimPunch( Angles punchAmount, float recoverySpeed = 5f ) + { + aimPunchAngle += punchAmount; + aimPunchRecoverySpeed = recoverySpeed; + timeSinceLastPunch = 0; + + ApplyEyeAnglesOffset( punchAmount ); + } + + /// + /// Reset aimpunch angle (called on weapon switch, death, etc.) + /// + public virtual void ResetAimPunch() + { + aimPunchAngle = Angles.Zero; + } + + /// + /// Handle aimpunch recovery over time. + /// Recovery only starts when player stops shooting. + /// + public virtual void HandleAimPunch() + { + if ( !IsAlive ) + { + aimPunchAngle = Angles.Zero; + return; + } + + bool isShooting = Inventory?.ActiveItem?.IsShooting() ?? false; + + if ( isShooting || timeSinceLastPunch < 0.1f ) + return; + + if ( aimPunchAngle.pitch != 0 || aimPunchAngle.yaw != 0 || aimPunchAngle.roll != 0 ) + { + var decayAmount = aimPunchRecoverySpeed * Time.Delta; + + var oldPitch = aimPunchAngle.pitch; + var oldYaw = aimPunchAngle.yaw; + var oldRoll = aimPunchAngle.roll; + + aimPunchAngle = new Angles( + aimPunchAngle.pitch.Approach( 0, decayAmount ), + aimPunchAngle.yaw.Approach( 0, decayAmount ), + aimPunchAngle.roll.Approach( 0, decayAmount ) + ); + + var deltaAngle = new Angles( + aimPunchAngle.pitch - oldPitch, + aimPunchAngle.yaw - oldYaw, + aimPunchAngle.roll - oldRoll + ); + + ApplyEyeAnglesOffset( deltaAngle ); + } + } +} diff --git a/code/swb_player/PlayerBase.cs b/code/swb_player/PlayerBase.cs index 88f985d..be2dd49 100644 --- a/code/swb_player/PlayerBase.cs +++ b/code/swb_player/PlayerBase.cs @@ -224,6 +224,7 @@ protected override void OnUpdate() ViewModelCamera.Enabled = IsFirstPerson && IsAlive; HandleFlinch(); HandleScreenShake(); + HandleAimPunch(); } if ( IsAlive ) @@ -259,7 +260,8 @@ public void TriggerAnimation( Animations animation ) public void ApplyEyeAnglesOffset( Angles offset ) { - CameraMovement?.EyeAnglesOffset += offset; + if ( CameraMovement is null ) return; + CameraMovement.EyeAnglesOffset += offset; } public void ParentToBone( GameObject weaponObject, string boneName ) diff --git a/code/swb_shared/IInventory.cs b/code/swb_shared/IInventory.cs index 5fe2fe9..e3434fc 100644 --- a/code/swb_shared/IInventory.cs +++ b/code/swb_shared/IInventory.cs @@ -36,4 +36,7 @@ public interface IInventoryItem : IValid /// Can the GameObject be switched out public bool CanCarryStop(); + + /// Is the item currently being used (e.g. weapon is firing) + public bool IsShooting(); } diff --git a/code/swb_shared/IPlayerBase.cs b/code/swb_shared/IPlayerBase.cs index a428e7b..8dfedc0 100644 --- a/code/swb_shared/IPlayerBase.cs +++ b/code/swb_shared/IPlayerBase.cs @@ -115,6 +115,14 @@ public interface IPlayerBase : IValid, Sandbox.Component.IDamageable /// The suggested angular offset to apply public void ApplyEyeAnglesOffset( Angles offset ); + /// + /// Apply aimpunch (camera recoil with recovery) to the player's view. + /// Unlike ApplyEyeAnglesOffset, this will smoothly recover over time. + /// + /// The angular offset to apply + /// How fast the camera recovers (higher = faster) + public void ApplyAimPunch( Angles punchAmount, float recoverySpeed = 5f ); + /// /// Called when the weapon object should be attached/parented to the player's body ///