swb_base/Weapon.Var.cs

Partial Weapon class declaration containing configurable properties and state variables for a weapon in SWB based project. Declares editable properties (models, sounds, firing, reload and animation settings), derived runtime booleans and TimeSince timers, plus some lists and nested types for scoping, offsets and bolt cycling.

NetworkingFile Access
using SWB.Shared;
using System;

namespace SWB.Base;

public partial class Weapon
{
	/// <summary>Unique name that identifies the weapon</summary>
	[Property, Group( "General" ), Order( 0 ), Feature( "Core", Icon = "hub" )] public string ClassName { get; set; }

	[Property, Group( "General" ), Feature( "Core" )] public string DisplayName { get; set; }

	[Property, Group( "General" ), Feature( "Core" ), ImageAssetPath] public string Icon { get; set; }

	[Property, Group( "General" ), Feature( "Core" )] public CrosshairSettings CrosshairSettings { get; set; } = new();

	/// <summary>How the player holds the weapon in thirdperson</summary>
	[Property, Group( "General" ), Feature( "Core" )] public HoldTypes HoldType { get; set; } = HoldTypes.Pistol;

	/// <summary>Can bullets be cocked in the barrel? (clip ammo + 1)</summary>
	[Property, Group( "General" ), Feature( "Core" )] public bool BulletCocking { get; set; } = true;

	/// <summary>Range that tucking should be enabled (-1 to disable tucking)</summary>
	[Property, Group( "General" ), Feature( "Core" )] public float TuckRange { get; set; } = 30f;

	/// <summary>How much movement speed is affected by holding this weapon (Speed *= Mobility)</summary>
	[Property, Group( "General" ), Feature( "Core" )] public float Mobility { get; set; } = 1f;

	/// <summary>
	/// How much of its movement speed the player keeps while AIMING this weapon — a multiplier on the movement
	/// speed, in place of the player's own `NZPlayer.AdsSpeedMultiplier` (0.5) for this weapon. 0 or below, the
	/// default, keeps the player's. Written by the Kitbash Editor's Stats panel ("aiming move speed").
	/// </summary>
	[Property, Group( "General" ), Feature( "Core" )] public float AdsMoveSpeed { get; set; } = -1f;

	/// <summary>A speed multiplier for all reloading animations</summary>
	[Property, Group( "General" ), Feature( "Core" ), Sync] public float ReloadSpeed { get; set; } = 1f;

	[Property, Group( "General" ), Feature( "Core" )] public int Slot { get; set; } = 0;

	/// <summary>View Model field of view</summary>
	[Property, Group( "General" ), Feature( "Core" )] public float ViewModelFOV { get; set; } = 70f;

	/// <summary>Aim Information</summary>
	[Property, Group( "General" ), Feature( "Core" )]
	public AimInfo AimInfo { get; set; } = new AimInfo()
	{
		Sensitivity = 0.85f,
	};

	/// <summary>Firstperson Model</summary>
	[Property, Group( "Models", Icon = "3d_rotation" ), Order( 1 ), Feature( "Core" )] public Model ViewModel { get; set; }

	/// <summary>Firstperson Hands Model</summary>
	[Property, Group( "Models" ), Feature( "Core" )] public Model ViewModelHands { get; set; }

	/// <summary>Thirdperson Model</summary>
	[Property, Group( "Models" ), Feature( "Core" )] public Model WorldModel { get; set; }

	/// <summary>Enable scoping, renders a 2D scope on ADS</summary>
	[Property, Group( "Scoping" ), Order( 5 ), Feature( "Core" )] public bool Scoping { get; set; } = false;

	/// <summary>Scope Information</summary>
	[Property, Group( "Scoping" ), Feature( "Core" )] public ScopeInfo ScopeInfo { get; set; } = new();

	/// <summary>Firing sound when clip is empty</summary>
	[Property, Group( "Sounds" ), Order( 9 ), Feature( "Core" )] public SoundEvent DeploySound { get; set; }

	/// <summary>Set when the draw sound is built in code: `DeploySound` is then its template. See GunCue.</summary>
	[Property, Group( "Sounds" ), Order( 9 ), Feature( "Core" )] public GunCue DeploySoundCue { get; set; }

	/// <summary>Primary attack data</summary>
	[Property, Group( "Firing" ), Order( 10 ), Feature( "Core" ), Title( "Primary ShootInfo (component)" ), RequireComponent] public ShootInfo Primary { get; set; }

	/// <summary>Secondary attack data (setting this will disable weapon aiming)</summary>
	[Property, Group( "Firing" ), Feature( "Core" ), Title( "Secondary ShootInfo (component)" )] public ShootInfo Secondary { get; set; }


	/// <summary>Procedural animation speed (lower is slower)</summary>
	[Property, Group( "General" ), Order( 0 ), Feature( "Animations", Icon = "animation" )] public float AnimSpeed { get; set; } = 1;

	/// <summary>
	/// ⚠️ ADDED FOR THIS PROJECT — not upstream SWB.
	///
	/// The HIP position: where the viewmodel sits when you are NOT aiming. SWB
	/// assumes the model is authored at the right place and only offers Aim, Run
	/// and Customize offsets — which is fine for a model made for s&box, and
	/// wrong for a PORTED one. An ARC9 `c_` model is authored around GMod's
	/// viewmodel origin, so it arrives correct in shape and badly placed on
	/// screen, and there was no field to correct it with.
	/// </summary>
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Hip Offset (swb_editor_offsets)" )] public AngPos ViewModelOffset { get; set; }

	/// <summary>Offset used for setting the weapon to its aim position</summary>
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Aim Offset (swb_editor_offsets)" )] public AngPos AimAnimData { get; set; }

	/// <summary>Offset used for setting the weapon to its run position</summary>
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Run Offset (swb_editor_offsets)" )] public AngPos RunAnimData { get; set; }

	/// <summary>
	/// Where this weapon's muzzle effects sit, relative to its `muzzle` attachment.
	/// </summary>
	///
	/// ⛔ PER WEAPON, NOT PROJECT-WIDE, AND THAT DISTINCTION IS THE WHOLE REASON IT IS HERE.
	/// `MuzzleFlash.Scale` corrects something all 496 prefabs got wrong together — an import
	/// default. A flash sitting off the barrel is one model's attachment being in the wrong place,
	/// and a global fix for that moves the other 495 off theirs.
	///
	/// ⚠️ IT SAVES THROUGH `WeaponPlacement` like the four poses above, so it survives a restart
	/// and ships in `weapons/placement.json` rather than living in a console session.
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Muzzle Offset (nz_muzzle_offset)" )] public AngPos MuzzleOffset { get; set; }

	/// <summary>
	/// Where this weapon's hands sit, relative to the viewmodel.
	/// </summary>
	///
	/// ⚠️ ZERO MEANS "EXACTLY ON THE VIEWMODEL", not "leave the hands alone" — see
	/// `SckPartsRig.ApplyHands`, which skips the whole override while this is zero so the bone
	/// merge keeps doing its job untouched.
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Hands Offset (nz_hands)" )] public AngPos HandsOffset { get; set; }

	/// <summary>Offset used for setting the weapon to its run position</summary>
	[Property, Group( "General" ), Feature( "Animations" ), Title( "Customizing Offset (swb_editor_offsets)" )] public AngPos CustomizeAnimData { get; set; }

	/// <summary>Duration of the reload animation</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public float ReloadTime { get; set; } = 1f;

	/// <summary>Reloading animation</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public string ReloadAnim { get; set; } = "reload";

	/// <summary>Duration of the empty reload animation (-1 to disable)</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public float ReloadEmptyTime { get; set; } = -1f;

	/// <summary>Reloading animation when clip is empty</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public string ReloadEmptyAnim { get; set; } = "reload_empty";

	/// <summary>Duration of the draw animation</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public float DrawTime { get; set; } = 0.5f;

	/// <summary>Draw animation</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public string DrawAnim { get; set; } = "deploy";

	/// <summary>
	/// Putting the weapon AWAY.
	///
	/// ⛔ ADDED FOR THIS PROJECT — SWB HAS NO HOLSTER AT ALL. There is not one
	/// reference to holstering anywhere in `swb_base`: weapons are drawn and then
	/// simply vanish when the next one appears. Every ported weapon has carried a
	/// compiled `holster` clip this whole time with nothing able to ask for it.
	///
	/// ⚠️ `holster`, not `holster_fast` — the guns ship `holster` and `holster_a`
	/// only. `holster_fast` exists on the KNIFE and assuming it was universal would
	/// hand every weapon a name its model does not have, which drops the renderer to
	/// the bind pose rather than erroring.
	/// </summary>
	[Property, Group( "General" ), Feature( "Animations" )] public string HolsterAnim { get; set; } = "holster";

	/// <summary>Duration of the empty draw animation (-1 to disable)</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public float DrawEmptyTime { get; set; } = -1f;

	/// <summary>Draw animation when there is no ammo</summary>
	[Property, Group( "General" ), Feature( "Animations" )] public string DrawEmptyAnim { get; set; } = "";


	/// <summary>Is the weapon reloading shells instead of a magazine?</summary>
	[Property, Group( "Shell Reloading" ), Order( 1 ), Feature( "Animations" )] public bool ShellReloading { get; set; } = false;

	/// <summary>Can the weapon shoot while reloading to cancel the reload?</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public bool ShellReloadingShootCancel { get; set; } = true;

	/// <summary>Delay in fire animation to eject the shell</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public float ShellEjectDelay { get; set; } = 0;

	/// <summary>Duration of the shell reload start animation (animation is set with ReloadAnim)</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public float ShellReloadStartTime { get; set; } = 0;

	/// <summary>Duration of the shell reload insert animation (animation is set in animgraph)</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public float ShellReloadInsertTime { get; set; } = 0;

	/// <summary>
	/// Clip played ONCE as the reload begins — bringing the gun down, opening the port.
	///
	/// ⛔ ADDED FOR THIS PROJECT. Upstream drives the insert from an ANIMGRAPH and
	/// only exposes ReloadAnim, so a model without one (every ARC9 port) can play
	/// the per-shell insert or the opening motion, but not both — the reload snaps
	/// in and out with no transition.
	/// </summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public string ShellReloadStartAnim { get; set; } = "";

	/// <summary>Clip played ONCE when the last shell is in — closing the port, raising the gun.</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public string ShellReloadEndAnim { get; set; } = "";

	/// <summary>Duration of the shell reload end animation.</summary>
	[Property, Group( "Shell Reloading" ), Feature( "Animations" )] public float ShellReloadEndTime { get; set; } = 0;


	/// <summary>
	/// Cycle the bolt after EVERY shot, not just on an empty reload.
	///
	/// ⛔ ADDED FOR THIS PROJECT. SWB's BoltBack only runs as part of a reload
	/// (`Primary.Ammo == 0 && BoltBack`), so a bolt-action rifle fired like a
	/// semi-auto with a rate cap — the bolt never moved between shots. ARC9 models
	/// this with ManualAction, which has no SWB equivalent.
	/// </summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public bool BoltActionPerShot { get; set; } = false;

	/// <summary>Clip for the per-shot cycle. Falls back to BoltBackAnim.</summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public string BoltCycleAnim { get; set; } = "";

	/// <summary>
	/// The per-shot cycle's sounds — the pump racked back and forward, the bolt lifted and run home — each at its REAL
	/// second from the moment the cycle clip starts.
	///
	/// ⛔ WITHOUT THEM A PUMP GUN CYCLES IN SILENCE. AsyncBoltCycle played only the clip; MW's `Rechamber` carries its
	/// sounds as model events inside the clip (Docs/MW_BASE_PORTING.md §5), and nothing else here could play them.
	/// ⚠️ REAL SECONDS, NOT THE 30-FPS SECONDS OF THE RELOAD CUES: the cycle clip is played unfitted, so nothing scales
	/// these. A cue at or past BoltBackTime never plays. Empty (every prefab that never set it) = silent, as before.
	/// </summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public System.Collections.Generic.List<ReloadCue> BoltCycleCues { get; set; } = new();

	/// <summary>Is this a bolt action weapon?</summary>
	[Property, Group( "Bolt Action Reloading" ), Order( 2 ), Feature( "Animations" ), Title( "Bolt Action" )] public bool BoltBack { get; set; } = false;

	/// <summary>Duration of the boltback animation</summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public float BoltBackTime { get; set; } = 0f;

	/// <summary>Boltback animation</summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public string BoltBackAnim { get; set; } = "boltback";

	/// <summary>Bullet eject delay during the boltback animation (-1 to disable)</summary>
	[Property, Group( "Bolt Action Reloading" ), Feature( "Animations" )] public float BoltBackEjectDelay { get; set; } = 0f;


	/// <summary>Time since the last primary attack</summary>
	public TimeSince TimeSincePrimaryShoot { get; set; }

	/// <summary>Time since the last secondary attack</summary>
	public TimeSince TimeSinceSecondaryShoot { get; set; }

	/// <summary>Time since deployment</summary>
	public TimeSince TimeSinceDeployed { get; set; }

	/// <summary>Time since the last reload</summary>
	public TimeSince TimeSinceReload { get; set; }

	/// <summary>Time since the weapon was in run animation</summary>
	public TimeSince TimeSinceRunning { get; set; }

	public bool IsCustomizing { get; set; }

	/// <summary>If the player is running</summary>
	// ⚠️ VENDORED SWB FILE — `new` removed, nothing else changed. It hid
	// nothing here (CS0109): upstream's Weapon derives from a base that
	// declares IsRunning, ours does not, so the keyword was inert.
	public bool IsRunning => Owner.IsRunning && Owner.IsOnGround && Owner.Velocity.Length >= 200 * Math.Min( Mobility, 1f );

	/// <summary>If the player is crouching</summary>
	public bool IsCrouching => Owner.IsCrouching;

	/// <summary>Is the view model visible</summary>
	public bool CanSeeViewModel => !IsProxy && Owner.IsFirstPerson;

	/// <summary>If the weapon is being reloaded</summary>
	[Sync] public bool IsReloading { get; set; }

	/// <summary>If the weapon is being aimed</summary>
	[Sync] public bool IsAiming { get; set; }

	/// <summary>If the weapon is being scoped</summary>
	[Sync] public bool IsScoping { get; set; }

	/// <summary>If the weapon is being bolt back reloaded</summary>
	[Sync] public bool InBoltBack { get; set; }

	// Scoping
	public static readonly Vector2 DefaultScopeLensCenter = new( 0.5f, 0.5f );
	public Vector2 ScopeLensCenter { get; private set; } = DefaultScopeLensCenter;
	public virtual void SetScopeLensCenter( Vector2 center ) => ScopeLensCenter = center;

	public StatsModifier InitialPrimaryStats { get; private set; }
	public StatsModifier InitialSecondaryStats { get; private set; }

	public bool IsDeploying => TimeSinceDeployed < 0;
	public bool ShouldTuckVar = false;
	public float TuckDist = -1;

	// Internal state
	protected int burstCount = 0;
	int barrelHeat = 0;
}