swb_base/Weapon.cs
using SWB.Base.Attachments;
using SWB.Shared;
using System.Collections.Generic;
using System.Linq;
namespace SWB.Base;
[Group( "SWB" )]
[Title( "Weapon" )]
public partial class Weapon : Component, IInventoryItem
{
public IPlayerBase Owner { get; private set; }
public ViewModelHandler ViewModelHandler { get; private set; }
public PlayerCameraHandler CameraHandler { get; private set; }
public SkinnedModelRenderer ViewModelRenderer { get; private set; }
public SkinnedModelRenderer ViewModelHandsRenderer { get; private set; }
public SkinnedModelRenderer WorldModelRenderer { get; private set; }
public WeaponSettings Settings { get; private set; }
public List<Attachment> Attachments = new();
protected override void OnAwake()
{
Tags.Add( TagsHelper.Weapon );
Attachments = Components.GetAll<Attachment>( FindMode.EverythingInSelf ).OrderBy( att => att.Name ).ToList();
Settings = WeaponSettings.Instance;
InitialPrimaryStats = StatsModifier.FromShootInfo( this, Primary );
// Default BulletType
if ( Primary is not null && Primary.BulletType is null )
Primary.BulletType = Components.Create<HitScanBulletInfo>();
if ( Secondary is not null && Secondary.BulletType is null )
Secondary.BulletType = Components.Create<HitScanBulletInfo>();
// Stats
if ( Secondary is not null )
InitialSecondaryStats = StatsModifier.FromShootInfo( this, Secondary );
else
InitialSecondaryStats = StatsModifier.Zero;
// Hack: Hide weapon object until position is set when creating world model
if ( !IsProxy )
{
WorldPosition = new( 0, 0, -999999 );
Network.ClearInterpolation();
}
Owner = Components.GetInAncestors<IPlayerBase>( true );
if ( !Owner.IsValid() )
{
Log.Error( $"{ClassName} cannot find owner, destroying!" );
Destroy();
}
}
protected override void OnDestroy()
{
ViewModelRenderer?.GameObject?.Destroy();
}
protected override void OnEnabled()
{
if ( IsProxy ) return;
if ( ViewModelRenderer?.GameObject is not null )
ViewModelRenderer.GameObject.Enabled = true;
ClearState();
if ( !Owner.IsBot )
CreateUI();
}
protected override void OnDisabled()
{
if ( IsProxy ) return;
if ( ViewModelRenderer?.GameObject is not null )
ViewModelRenderer.GameObject.Enabled = false;
if ( ViewModelHandler is not null )
ViewModelHandler.ShouldDraw = false;
// Attachments (VM + HUD)
Attachments.ForEach( ( att ) =>
{
if ( att.Equipped )
{
if ( att.ViewModelRenderer is not null )
att.ViewModelRenderer.Enabled = false;
if ( att.CreatedUI )
att.DestroyHudElements();
}
} );
ClearState();
if ( Owner is not null )
Owner.HoldType = HoldTypes.None;
DestroyUI();
}
protected virtual void ClearState()
{
IsReloading = false;
IsScoping = false;
IsAiming = false;
IsCustomizing = false;
SetScopeLensCenter( DefaultScopeLensCenter );
}
[Rpc.Broadcast]
public virtual void OnCarryStart()
{
if ( !GameObject.IsValid() || !this.IsValid() ) return;
GameObject.Enabled = true;
TimeSinceDeployed = -999f;
}
[Rpc.Broadcast]
public virtual void OnCarryStop()
{
if ( !GameObject.IsValid() || !this.IsValid() ) return;
GameObject.Enabled = false;
}
public virtual bool CanCarryStop()
{
return Owner.IsBot || TimeSinceDeployed > 0;
}
public virtual (float delay, string anim) GetDrawInfo()
{
var delay = 0f;
var anim = "";
if ( Primary.Ammo == 0 && !string.IsNullOrEmpty( DrawEmptyAnim ) )
{
anim = DrawEmptyAnim;
delay = DrawEmptyTime;
}
else if ( !string.IsNullOrEmpty( DrawAnim ) )
{
anim = DrawAnim;
delay = DrawTime;
}
return (delay, anim);
}
public virtual void OnDeploy()
{
var drawInfo = GetDrawInfo();
TimeSinceDeployed = -drawInfo.delay;
// Sound
if ( !IsProxy && DeploySound is not null )
PlaySound( DeploySound );
// Boltback
if ( InBoltBack )
AsyncBoltBack( drawInfo.delay );
}
public virtual void OnViewModelDeploy()
{
var drawInfo = GetDrawInfo();
// Reset playback rate
ViewModelRenderer?.PlaybackRate = 1;
if ( !string.IsNullOrEmpty( drawInfo.anim ) )
ViewModelRenderer?.Set( drawInfo.anim, true );
// Start drawing (We delay by 1 frame to allow the animation to start first)
async void ShouldDrawDelayed()
{
await GameTask.Delay( 100 );
if ( ViewModelHandler.IsValid() )
{
ViewModelHandler.ShouldDraw = true;
OnViewModelDrawn();
}
}
ShouldDrawDelayed();
}
/// <summary>Called when the view model starts being drawn</summary>
public virtual void OnViewModelDrawn() { }
protected override void OnStart()
{
if ( !IsProxy && Owner.Camera is not null )
{
CameraHandler = Components.GetOrCreate<PlayerCameraHandler>();
CameraHandler.Weapon = this;
}
CreateModels();
// Attachments (enabled via property)
if ( !IsProxy )
{
Attachments.ForEach( att =>
{
if ( att.Enable && !att.Equipped )
att.EquipBroadCast();
} );
}
// Attachments (load for clients joining late)
if ( IsProxy )
{
// Log.Info( "Checking -> " + Network.Owner.DisplayName + "'s " + DisplayName + " for attachments" );
Attachments.ForEach( att =>
{
// Log.Info( "[" + att.Name + "] equipped ->" + att.Equipped );
if ( att is not null && att.Equipped )
att.Equip();
} );
}
}
protected override void OnFixedUpdate()
{
if ( !IsProxy && !IsDeploying && Owner.IsValid() && !Owner.IsBot && TuckRange != -1 )
{
ShouldTuckVar = ShouldTuck( out TuckDist );
}
}
protected override void OnUpdate()
{
if ( !Owner.IsValid() ) return;
UpdateModels();
Owner.HoldType = HoldType;
if ( !IsProxy && !Owner.IsBot && !IsDeploying )
{
// Customization
if ( WeaponSettings.Instance.Customization && !IsScoping && !IsAiming && Input.Pressed( InputButtonHelper.Menu ) && Attachments.Count > 0 )
{
if ( !IsCustomizing )
OpenCustomizationMenu();
else
CloseCustomizationMenu();
IsCustomizing = !IsCustomizing;
}
// Don't cancel reload when customizing
if ( IsCustomizing && !IsReloading ) return;
if ( IsRunning )
TimeSinceRunning = 0;
var wasAiming = IsAiming;
IsAiming = !Owner.IsRunning && AimAnimData != AngPos.Zero && Input.Down( InputButtonHelper.SecondaryAttack );
if ( wasAiming != IsAiming )
{
if ( IsAiming )
OnAimStart();
else
OnAimStop();
}
if ( IsScoping )
Owner.InputSensitivity = ScopeInfo.Sensitivity;
else if ( IsAiming )
Owner.InputSensitivity = AimInfo.Sensitivity;
else
Owner.InputSensitivity = 1f;
OnAimAssistUpdate();
if ( IsAiming )
OnAimUpdate();
if ( Scoping )
{
if ( IsAiming && !IsScoping )
OnScopeStart();
else if ( !IsAiming && IsScoping )
OnScopeEnd();
}
ResetBurstFireCount( Primary, InputButtonHelper.PrimaryAttack );
ResetBurstFireCount( Secondary, InputButtonHelper.SecondaryAttack );
BarrelHeatCheck();
if ( CanPrimaryShoot() && !ShouldTuckVar )
{
if ( IsReloading && ShellReloading && ShellReloadingShootCancel )
CancelShellReload();
TimeSincePrimaryShoot = 0;
Shoot( Primary, true );
}
else if ( CanSecondaryShoot() && !ShouldTuckVar )
{
TimeSinceSecondaryShoot = 0;
Shoot( Secondary, false );
}
else if ( Input.Down( InputButtonHelper.Reload ) )
{
if ( ShellReloading )
OnShellReload();
else
Reload();
}
if ( IsReloading && TimeSinceReload >= 0 )
{
if ( ShellReloading )
OnShellReloadFinish();
else
OnReloadFinish();
}
}
}
void UpdateModels()
{
// Should draw after deploy
if ( (IsProxy || Owner.IsBot) && WorldModelRenderer is not null )
{
if ( WorldModelRenderer.RenderType != ModelRenderer.ShadowRenderType.On )
WorldModelRenderer.RenderType = ModelRenderer.ShadowRenderType.On;
if ( !WorldModelRenderer.RenderOptions.Game )
WorldModelRenderer.RenderOptions.Game = true;
}
if ( !IsProxy && !Owner.IsBot && WorldModelRenderer is not null )
{
var worldModelRenderType = Owner.IsFirstPerson ? ModelRenderer.ShadowRenderType.ShadowsOnly : ModelRenderer.ShadowRenderType.On;
if ( WorldModelRenderer.RenderType != worldModelRenderType )
WorldModelRenderer.RenderType = worldModelRenderType;
// Should draw after deploy
if ( !Owner.IsFirstPerson && !WorldModelRenderer.RenderOptions.Game )
WorldModelRenderer.RenderOptions.Game = true;
// Attachments
Attachments.ForEach( ( att ) =>
{
if ( !att.Equipped ) return;
if ( att.ViewModelRenderer.IsValid() )
att.ViewModelRenderer.Enabled = Owner.IsFirstPerson && ViewModelHandler.ShouldDraw;
if ( att.WorldModelRenderer.IsValid() && att.WorldModelRenderer.RenderType != worldModelRenderType )
att.WorldModelRenderer.RenderType = worldModelRenderType;
} );
}
}
/// <summary>Override to use a custom ViewModelHandler</summary>
protected virtual ViewModelHandler CreateViewModelHandler( GameObject go )
{
return go.Components.Create<ViewModelHandler>();
}
void CreateModels()
{
if ( !IsProxy && !Owner.IsBot && ViewModel.IsValid() && !ViewModelRenderer.IsValid() )
{
var viewModelGO = new GameObject( true, "Viewmodel - " + ClassName );
viewModelGO.SetParent( Owner.GameObject, false );
viewModelGO.Tags.Add( TagsHelper.ViewModel );
viewModelGO.NetworkMode = NetworkMode.Never;
ViewModelRenderer = viewModelGO.Components.Create<SkinnedModelRenderer>();
ViewModelRenderer.Model = ViewModel;
ViewModelRenderer.AnimationGraph = ViewModel.AnimGraph;
ViewModelRenderer.CreateBoneObjects = true;
ViewModelRenderer.CreateAttachments = true;
ViewModelRenderer.Enabled = false;
ViewModelRenderer.OnSoundEvent += ( sceneSound ) =>
{
var soundEvent = ResourceLibrary.Get<SoundEvent>( sceneSound.Name );
if ( soundEvent is null ) return;
// Make sure local always has UI sound
soundEvent.UI = true;
soundEvent.Volume = 1;
using ( Rpc.FilterExclude( Owner.GameObject.Network.Owner ) )
{
PlaySound( soundEvent, 0.5f, 7500f, true );
}
};
ViewModelRenderer.OnComponentEnabled += async () =>
{
// Prevent flickering when enabling the component, this is controlled by the ViewModelHandler
ViewModelRenderer.RenderType = ModelRenderer.ShadowRenderType.ShadowsOnly;
ViewModelRenderer.ClearParameters();
OnViewModelDeploy();
// Deploy
if ( WorldModel is null )
{
await GameTask.DelayRealtime( 1 );
if ( this.IsValid() )
OnDeploy();
}
};
ViewModelHandler = CreateViewModelHandler( viewModelGO );
ViewModelHandler.Weapon = this;
ViewModelHandler.ViewModelRenderer = ViewModelRenderer;
var viewModelCamera = Owner.ViewModelCamera;
if ( Owner.ViewModelCamera is null )
{
var viewModelCameraGameObject = new GameObject();
viewModelCameraGameObject.Name = "ViewModelCamera";
viewModelCameraGameObject.SetParent( Owner.GameObject, false );
// Setup the view model camera
viewModelCamera = viewModelCameraGameObject.Components.Create<CameraComponent>();
viewModelCamera.ClearFlags = ClearFlags.Depth | ClearFlags.Stencil;
viewModelCamera.ZNear = 1;
viewModelCamera.Priority = 2;
viewModelCamera.TargetEye = StereoTargetEye.RightEye;
viewModelCamera.RenderTags.Add( new TagSet() { TagsHelper.ViewModel, TagsHelper.Light } );
Owner.ViewModelCamera = viewModelCamera;
}
ViewModelHandler.Camera = viewModelCamera;
Owner.Camera.RenderExcludeTags.Add( TagsHelper.ViewModel );
if ( ViewModelHands is not null )
{
ViewModelHandsRenderer = viewModelGO.Components.Create<SkinnedModelRenderer>();
ViewModelHandsRenderer.Model = ViewModelHands;
ViewModelHandsRenderer.BoneMergeTarget = ViewModelRenderer;
ViewModelHandsRenderer.OnComponentEnabled += () =>
{
// Prevent flickering when enabling the component, this is controlled by the ViewModelHandler
ViewModelHandsRenderer.RenderType = ModelRenderer.ShadowRenderType.ShadowsOnly;
};
}
ViewModelHandler.ViewModelHandsRenderer = ViewModelHandsRenderer;
}
if ( WorldModel is not null && WorldModelRenderer is null )
{
WorldModelRenderer = Components.Create<SkinnedModelRenderer>();
WorldModelRenderer.Model = WorldModel;
WorldModelRenderer.AnimationGraph = WorldModel.AnimGraph;
WorldModelRenderer.CreateBoneObjects = true;
WorldModelRenderer.CreateAttachments = true;
async void OnComponentEnabled()
{
// Prevent flickering when enabling the component
WorldModelRenderer.RenderType = ModelRenderer.ShadowRenderType.Off;
WorldModelRenderer.RenderOptions.Game = false;
// Deploy
await GameTask.DelayRealtime( 1 );
if ( this.IsValid() )
OnDeploy();
}
WorldModelRenderer.OnComponentEnabled += () =>
{
// Called after weapon has been switched already
OnComponentEnabled();
};
// Called when weapon models are created
OnComponentEnabled();
Owner.ParentToBone( GameObject, "hold_R", deleteOnFail: false );
}
}
[Rpc.Broadcast]
public void PlaySound( SoundEvent sound, float volume = float.NaN, float distance = float.NaN, bool shouldFollow = false )
{
if ( sound is null || !this.IsValid() ) return;
var isScreenSound = CanSeeViewModel;
sound.UI = isScreenSound;
if ( !float.IsNaN( volume ) )
sound.Volume = volume;
if ( !float.IsNaN( distance ) )
sound.Distance = distance;
if ( isScreenSound )
Sound.Play( sound );
else
{
var handle = Sound.Play( sound, WorldPosition );
if ( shouldFollow )
{
handle?.Parent = this.GameObject;
handle?.FollowParent = true;
}
else
{
handle?.Position = WorldPosition;
}
}
}
[Rpc.Broadcast]
public void PlayWorldSound( string eventName, float volume = 1, float distance = 7500 )
{
var handle = Sound.Play( eventName, WorldPosition );
handle.Volume = volume;
handle.Distance = distance;
}
}