using System; using Sandbox; using System.Collections.Generic; namespace LuckerGame.Entities.Weapons; public partial class Weapon : AnimatedEntity { /// /// The View Model's entity, only accessible clientside. /// public WeaponViewModel ViewModelEntity { get; protected set; } /// /// An accessor to grab our Pawn. /// public Racer Pawn => Owner as Racer; /// /// This'll decide which entity to fire effects from. If we're in first person, the View Model, otherwise, this. /// public AnimatedEntity EffectEntity => Camera.FirstPersonViewer == Owner ? ViewModelEntity : this; public virtual string ViewModelPath => null; public virtual string ModelPath => null; public virtual string ReloadSoundPath => null; public virtual string ReloadAnimPath => "reload"; public virtual string WeaponName => "weapon"; public virtual float ReloadDuration => 4f; public virtual CitizenAnimationHelper.HoldTypes HoldType { get; } protected virtual List HitTags => new List { "solid", "player", "npc" }; protected virtual float Damage => 20f; private bool Reloading => TimeSinceReloadStarted < ReloadDuration; /// /// How often you can shoot this gun. /// public virtual float PrimaryRate => 5.0f; /// /// How long since we last shot this gun. /// [Net, Predicted] public TimeSince TimeSincePrimaryAttack { get; set; } [Net, Predicted] public TimeSince TimeSinceReloadStarted { get; set; } public virtual int MaxAmmo { get; } [Net] public int Ammo { get; set; } public override void Spawn() { EnableHideInFirstPerson = true; EnableShadowInFirstPerson = true; EnableDrawing = false; Ammo = MaxAmmo; if ( ModelPath != null ) { SetModel( ModelPath ); } } /// /// Called when is called for this weapon. /// /// public void OnEquip( Racer pawn ) { Owner = pawn; SetParent( pawn, true ); EnableDrawing = true; CreateViewModel( To.Single( pawn ) ); pawn.SetAnimParameter( "holdtype", (int)HoldType ); } /// /// Called when the weapon is either removed from the player, or holstered. /// public void OnHolster() { EnableDrawing = false; DestroyViewModel( To.Single( Owner ) ); Pawn.SetAnimParameter( "holdtype", (int)CitizenAnimationHelper.HoldTypes.None ); } /// /// Called from . /// /// public override void Simulate( IClient player ) { Animate(); if ( CanPrimaryAttack() ) { using ( LagCompensation() ) { TimeSincePrimaryAttack = 0; ReduceAmmoAndPrimaryAttack(); } } else if ( CanReload() ) { Reload(); } } [ClientRpc] protected virtual void ReloadEffects() { Game.AssertClient(); ViewModelEntity?.SetAnimParameter( "reload", true ); } private void Reload() { ReloadEffects(); Pawn.PlaySound( ReloadSoundPath ); Ammo = MaxAmmo; Log.Info( $"Reload duration: {ReloadDuration}" ); TimeSinceReloadStarted = 0; } private bool CanReload() { return Owner.IsValid && Input.Down( "reload" ) && Ammo < MaxAmmo && !Reloading; } /// /// Called every to see if we can shoot our gun. /// /// public virtual bool CanPrimaryAttack() { if ( !Owner.IsValid() || !Input.Down( "attack1" ) || (Ammo == 0 && MaxAmmo != 0) || Reloading ) return false; var rate = PrimaryRate; if ( rate <= 0 ) return true; return TimeSincePrimaryAttack > (1 / rate); } private void ReduceAmmoAndPrimaryAttack() { Ammo = Math.Max(0, Ammo -1); PrimaryAttack(); } /// /// Called when your gun shoots. /// public virtual void PrimaryAttack() { } /// /// Useful for setting anim parameters based off the current weapon. /// protected virtual void Animate() { } /// /// Does a trace from start to end, does bullet impact effects. Coded as an IEnumerable so you can return multiple /// hits, like if you're going through layers or ricocheting or something. /// public virtual IEnumerable TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f ) { bool underWater = Trace.TestPoint( start, "water" ); var trace = Trace.Ray( start, end ) .UseHitboxes() .WithAnyTags( HitTags.ToArray() ) .Ignore( this ) .Size( radius ); // // If we're not underwater then we can hit water // if ( !underWater ) trace = trace.WithAnyTags( "water" ); var tr = trace.Run(); if ( tr.Hit ) yield return tr; } /// /// Shoot a single bullet /// public virtual void ShootBullet( Vector3 pos, Vector3 dir, float spread, float force, float damage, float bulletSize ) { var forward = dir; forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * spread * 0.25f; forward = forward.Normal; // // ShootBullet is coded in a way where we can have bullets pass through shit // or bounce off shit, in which case it'll return multiple results // foreach ( var tr in TraceBullet( pos, pos + forward * 5000, bulletSize ) ) { tr.Surface.DoBulletImpact( tr ); if ( !Game.IsServer ) continue; if ( !tr.Entity.IsValid() ) continue; // // We turn predictiuon off for this, so any exploding effects don't get culled etc // using ( Prediction.Off() ) { var damageInfo = DamageInfo.FromBullet( tr.EndPosition, forward * 100 * force, damage ) .UsingTraceResult( tr ) .WithAttacker( Owner ) .WithWeapon( this ); tr.Entity.TakeDamage( damageInfo ); } } } /// /// Shoot a single bullet from owners view point /// public virtual void ShootBullet( float spread, float force, float damage, float bulletSize ) { Game.SetRandomSeed( Time.Tick ); var ray = Owner.AimRay; ShootBullet( ray.Position, ray.Forward, spread, force, damage, bulletSize ); } [ClientRpc] public void CreateViewModel() { if ( ViewModelPath == null ) return; var vm = new WeaponViewModel( this ); vm.Model = Model.Load( ViewModelPath ); ViewModelEntity = vm; } [ClientRpc] public void DestroyViewModel() { if ( ViewModelEntity.IsValid() ) { ViewModelEntity.Delete(); } } }