SWB/swb_base/Weapon.Getters.cs

Partial Weapon class with getter and helper methods. Resolves effect renderers and muzzle attachment transforms, picks shoot animations from ShootInfo, computes ammo state, RPM timing, spread and recoil, and builds movement impact data from force.

using SWB.Base.Attachments;
using SWB.Shared;
using System;
using System.Collections.Generic;

namespace SWB.Base;

public partial class Weapon
{
	public virtual SkinnedModelRenderer GetEffectRenderer()
	{
		SkinnedModelRenderer effectModel = WorldModelRenderer;

		if ( CanSeeViewModel )
			effectModel = ViewModelRenderer;

		return effectModel;
	}

	/// <summary>
	/// Gets the visible muzzle renderer and attachment name
	/// </summary>
	public virtual (SkinnedModelRenderer, string) GetMuzzleEffectDetails()
	{
		var activeAttachment = GetActiveAttachmentForCategory( AttachmentCategory.Muzzle );
		var effectRenderer = GetEffectRenderer();
		var effectAttachment = "muzzle";

		if ( activeAttachment is not null )
		{
			effectAttachment = activeAttachment.EffectAttachmentOrBone;

			// If we have custom models use the attachment from those models instead of the main weapon one
			if ( CanSeeViewModel && activeAttachment.ViewModelRenderer is not null )
			{
				effectRenderer = activeAttachment.ViewModelRenderer;
			}
			else if ( !CanSeeViewModel && activeAttachment.WorldModelRenderer is not null )
			{
				effectRenderer = activeAttachment.WorldModelRenderer;
			}
		}

		return (effectRenderer, effectAttachment);
	}

	/// <summary>
	/// Gets the muzzle attachment transform
	/// </summary>
	public virtual Transform? GetMuzzleTransform()
	{
		var muzzleDetails = GetMuzzleEffectDetails();
		return muzzleDetails.Item1?.GetAttachment( muzzleDetails.Item2 );
	}

	/// <summary>
	/// Gets the muzzle attachment gameobject
	/// </summary>
	public virtual GameObject GetMuzzleObject()
	{
		var muzzleDetails = GetMuzzleEffectDetails();
		return muzzleDetails.Item1?.GetAttachmentObject( muzzleDetails.Item2 );
	}

	/// <summary>
	/// Gets the correct shoot animation
	/// </summary>
	/// <param name="shootInfo">Info used for the current attack</param>
	/// <returns></returns>
	public virtual string GetShootAnimation( ShootInfo shootInfo )
	{
		if ( IsAiming && (!string.IsNullOrEmpty( shootInfo.ShootAimedAnim )) )
		{
			return shootInfo.ShootAimedAnim;
		}
		else if ( shootInfo.Ammo == 0 && !string.IsNullOrEmpty( shootInfo.ShootEmptyAnim ) )
		{
			return shootInfo.ShootEmptyAnim;
		}

		return shootInfo.ShootAnim;
	}

	/// <summary>
	/// If there is usable ammo left
	/// </summary>
	public bool HasAmmo()
	{
		if ( Primary.InfiniteAmmo == InfiniteAmmoType.clip )
			return true;

		if ( Primary.ClipSize == -1 )
		{
			return Owner.AmmoCount( Primary.AmmoType ) > 0;
		}

		if ( Primary.Ammo == 0 )
			return false;

		return true;
	}

	public ShootInfo GetShootInfo( bool isPrimary )
	{
		return isPrimary ? Primary : Secondary;
	}

	public bool IsShooting()
	{
		if ( Secondary is null )
			return GetRealRPM( Primary.RPM ) > TimeSincePrimaryShoot;

		return GetRealRPM( Primary.RPM ) > TimeSincePrimaryShoot || GetRealRPM( Secondary.RPM ) > TimeSinceSecondaryShoot;
	}

	public static float GetRealRPM( int rpm )
	{
		return 60f / rpm;
	}

	public virtual float GetRealSpread( float baseSpread = -1 )
	{
		if ( !Owner.IsValid() ) return 0;

		float spread = baseSpread != -1 ? baseSpread : Primary.Spread;
		float floatMod = 1f;

		// Ducking
		if ( IsCrouching && !IsAiming )
			floatMod -= 0.25f;

		// Aiming
		if ( IsAiming && Primary.Bullets == 1 )
			floatMod *= AimInfo.SpreadModifier;

		if ( !Owner.IsOnGround )
		{
			// Jumping
			floatMod += 0.75f;
		}
		else if ( Owner.Velocity.Length > 100 )
		{
			// Moving 
			floatMod += 0.25f;
		}

		var scopeMultiplier = IsScoping ? ScopeInfo.Spread : 1f;
		return spread * floatMod * scopeMultiplier;
	}

	public virtual Angles GetRecoilAngles( ShootInfo shootInfo )
	{
		var recoilX = IsAiming ? -shootInfo.Recoil * 0.4f : -shootInfo.Recoil;
		var recoilY = Game.Random.NextFloat( -0.2f, 0.2f ) * recoilX;
		var recoilAngles = new Angles( recoilX, recoilY, 0 );
		return recoilAngles;
	}

	/// <summary>Pass extra details when killing a player</summary>
	public virtual Dictionary<string, string> GetKillDetails()
	{
		return null;
	}

	public static MovementImpact GetMovementImpactFromForce( float force )
	{
		// 1.0 → 0.6 (40% max slow)
		var maxSlow = 0.4f;
		var t = Math.Clamp( force / 10f, 0f, 1f );
		var amount = 1f - t * maxSlow;

		return new()
		{
			Amount = amount,
			Duration = 0.4f,
		};
	}
}