Player/Player.cs
using Sandbox.CameraNoise;
using static Sandbox.Component;
/// <summary>
/// Holds player information like health
/// </summary>
public sealed partial class Player : Component, IDamageable, PlayerController.IEvents
{
public static Player Local => Game.ActiveScene.GetAllComponents<Player>().Where( x => x.IsLocalPlayer ).FirstOrDefault();
[RequireComponent] public PlayerController Controller { get; set; }
[RequireComponent] public PlayerInventory Inventory { get; set; }
[Property] public GameObject Body { get; set; }
[Property, Range( 0, 100 ), Sync( SyncFlags.FromHost )] public float Health { get; set; } = 100;
[Property, Range( 0, 100 ), Sync( SyncFlags.FromHost )] public float MaxHealth { get; set; } = 100;
[Property, Range( 0, 100 ), Sync( SyncFlags.FromHost )] public float Armour { get; set; } = 0;
[Property, Range( 0, 100 ), Sync( SyncFlags.FromHost )] public float MaxArmour { get; set; } = 100;
[Sync( SyncFlags.FromHost )] public PlayerData PlayerData { get; set; }
public Transform EyeTransform
{
get
{
Assert.True( Controller.IsValid(), $"Player {DisplayName}'s PlayerController is invalid (IsValid: {this.IsValid()}, IsLocalPlayer: {IsLocalPlayer}, IsHost: {Networking.IsHost}, IsActive: {PlayerData?.Connection?.IsActive})" );
return Controller.EyeTransform;
}
}
public bool IsBot => PlayerData.BotId != -1;
public bool IsLocalPlayer => !IsProxy && !IsBot;
public Guid PlayerId => PlayerData.PlayerId;
public long SteamId => PlayerData.SteamId;
public string DisplayName => PlayerData.DisplayName;
protected override void OnFixedUpdate()
{
if ( !IsProxy )
{
ControlSpray();
}
}
protected override void OnStart()
{
var targets = Scene.GetAllComponents<DeathCameraTarget>()
.Where( x => x.Connection == Network.Owner );
// We don't care about spectating corpses once we spawn
foreach ( var t in targets )
{
t.GameObject.Destroy();
}
}
/// <summary>
/// Try to inherit transforms from the player onto its new ragdoll
/// </summary>
/// <param name="ragdoll"></param>
private void CopyBoneScalesToRagdoll( GameObject ragdoll )
{
// we are only interested in the bones of the player, not anything that may be attached to it.
var playerRenderer = Body.GetComponent<SkinnedModelRenderer>();
var bones = playerRenderer.Model.Bones;
var ragdollRenderer = ragdoll.GetComponent<SkinnedModelRenderer>();
ragdollRenderer.CreateBoneObjects = true;
var ragdollObjects = ragdoll.GetAllObjects( true ).ToLookup( x => x.Name );
foreach ( var bone in bones.AllBones )
{
var boneName = bone.Name;
if ( !ragdollObjects.Contains( boneName ) )
continue;
var boneObject = playerRenderer.GetBoneObject( boneName );
if ( !boneObject.IsValid() )
{
continue;
}
var boneOnRagdoll = ragdollObjects[boneName].FirstOrDefault();
if ( boneOnRagdoll.IsValid() && boneObject.WorldScale != Vector3.One )
{
boneOnRagdoll.Flags = boneOnRagdoll.Flags.WithFlag( GameObjectFlags.ProceduralBone, true );
boneOnRagdoll.WorldScale = boneObject.WorldScale;
var z = boneOnRagdoll.Parent;
z.Flags = z.Flags.WithFlag( GameObjectFlags.ProceduralBone, true );
z.WorldScale = boneObject.WorldScale;
}
}
}
/// <summary>
/// Creates a ragdoll but it isn't enabled
/// </summary>
[Rpc.Broadcast( NetFlags.HostOnly | NetFlags.Reliable )]
void CreateRagdoll()
{
if ( Application.IsDedicatedServer ) return;
var ragdoll = Controller.CreateRagdoll();
if ( !ragdoll.IsValid() ) return;
CopyBoneScalesToRagdoll( ragdoll );
var corpse = ragdoll.AddComponent<DeathCameraTarget>();
corpse.Connection = Network.Owner;
corpse.Created = DateTime.Now;
}
void CreateRagdollAndGhost()
{
var go = new GameObject( false, "Observer" );
go.Components.Create<PlayerObserver>();
go.NetworkSpawn( Network.Owner );
}
/// <summary>
/// Broadcasts death to other players
/// </summary>
[Rpc.Broadcast( NetFlags.HostOnly | NetFlags.Reliable )]
void NotifyDeath( IPlayerEvent.DiedParams args )
{
IPlayerEvent.PostToGameObject( GameObject, x => x.OnDied( args ) );
if ( args.Attacker == GameObject )
{
IPlayerEvent.PostToGameObject( GameObject, x => x.OnSuicide() );
}
}
[Rpc.Owner( NetFlags.HostOnly )]
private void Flatline()
{
Sound.Play( "audio/sounds/flatline.sound" );
}
private void Ghost()
{
//
// If we're in control of the player, we want to ghost, and respawn in a few seconds.
//
if ( !IsBot )
{
CreateRagdollAndGhost();
}
else
{
GameManager.Current.SpawnPlayerDelayed( PlayerData );
}
}
/// <summary>
/// Called on the host when a player dies
/// </summary>
void Kill( in DamageInfo d )
{
//
// Play the flatline sound on the owner
//
if ( IsLocalPlayer )
{
Flatline();
}
//
// Let everyone know about the death
//
NotifyDeath( new IPlayerEvent.DiedParams() { Attacker = d.Attacker } );
var inventory = GetComponent<PlayerInventory>();
if ( inventory.IsValid() )
{
inventory.SwitchWeapon( null );
inventory.DropCoffin();
}
if ( d.Tags.HasAny( DamageTags.Crush, DamageTags.Explosion, DamageTags.GibAlways ) )
{
Gib( d.Position, d.Origin );
}
else
{
CreateRagdoll();
}
//
// Ghost and say goodbye to the player
//
Ghost();
GameObject.Destroy();
}
[Rpc.Owner]
public void EquipBestWeapon()
{
var inventory = GetComponent<PlayerInventory>();
if ( inventory.IsValid() )
inventory.SwitchWeapon( inventory.GetBestWeapon() );
}
protected override void OnUpdate()
{
if ( IsLocalPlayer )
OnControl();
// If we're the host and we're not in the game stage then we should delete this player...
if ( !Networking.IsHost || GameManager.Current.CurrentGameStage == GameManager.GameStage.Game )
return;
GameObject.Destroy();
}
void OnControl()
{
/*
if ( Input.Pressed( "die" ) )
{
KillSelf();
}
*/
}
[ConCmd( "sbdm.dev.sethp", ConVarFlags.Cheat )]
private static void Dev_SetHp( int hp )
{
Player.Local.Health = hp;
}
private SoundHandle _dmgSound;
[Rpc.Broadcast( NetFlags.HostOnly | NetFlags.Reliable )]
private void NotifyOnDamage( IPlayerEvent.DamageParams args )
{
IPlayerEvent.PostToGameObject( GameObject, x => x.OnDamage( args ) );
// Effects.Current.SpawnBlood( args.Position, (args.Origin - args.Position).Normal, args.Damage );
if ( IsLocalPlayer )
{
_dmgSound?.Stop();
if ( args.Tags.Contains( DamageTags.Shock ) )
{
_dmgSound = Sound.Play( "damage_taken_shock" );
}
else
{
_dmgSound = Sound.Play( "damage_taken_shot" );
}
}
}
public void OnDamage( in DamageInfo dmg )
{
if ( Health < 1 ) return;
if ( PlayerData.IsGodMode ) return;
var damage = dmg.Damage;
if ( dmg.Tags.Contains( DamageTags.Headshot ) )
damage *= GameSettings.HeadshotDamageScale;
if ( Armour > 0 )
{
float remainingDamage = damage - Armour;
Armour = Math.Max( 0, Armour - damage );
damage = Math.Max( 0, remainingDamage );
}
Health -= damage;
NotifyOnDamage( new IPlayerEvent.DamageParams()
{
Damage = damage,
Attacker = dmg.Attacker,
Weapon = dmg.Weapon,
Tags = dmg.Tags,
Position = dmg.Position,
Origin = dmg.Origin,
} );
// We didn't die
if ( Health >= 1 ) return;
GameManager.Current.OnDeath( this, dmg );
Health = 0;
Kill( dmg );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void Gib( Vector3 hitPos, Vector3 origin )
{
var gibList = new List<PlayerGib>( GetComponentsInChildren<PlayerGib>( true ) );
DeathCameraTarget target = null;
foreach ( var g in gibList )
{
// Death camera target is the first gib
if ( !target.IsValid() )
{
target = g.AddComponent<DeathCameraTarget>();
target.Connection = Network.Owner;
target.Created = DateTime.Now;
}
g.Gib( origin, hitPos, noShrink: true );
}
// Effects.Current.SpawnBlood( WorldPosition, Vector3.Up, 500.0f );
}
void PlayerController.IEvents.OnEyeAngles( ref Angles ang )
{
var player = Components.Get<Player>();
var angles = ang;
IPlayerEvent.Post( x => x.OnCameraMove( ref angles ) );
ang = angles;
}
void PlayerController.IEvents.PostCameraSetup( CameraComponent camera )
{
// Set up initial field of view from preferences
camera.FovAxis = CameraComponent.Axis.Vertical;
camera.FieldOfView = Screen.CreateVerticalFieldOfView( Preferences.FieldOfView, 9.0f / 16.0f );
IPlayerEvent.Post( x => x.OnCameraSetup( camera ) );
ApplyMovementCameraEffects( camera );
IPlayerEvent.Post( x => x.OnCameraPostSetup( camera ) );
}
float roll;
private void ApplyMovementCameraEffects( CameraComponent camera )
{
if ( Controller.ThirdPerson ) return;
if ( !GamePreferences.ViewBobbing ) return;
var scaler = Controller.WishVelocity.Length.Remap( 0, Controller.RunSpeed, 0, 1 );
// side movement
var r = Controller.WishVelocity.Dot( EyeTransform.Left ) / -250.0f;
roll = MathX.Lerp( roll, r, Time.Delta * 10.0f, true );
camera.WorldRotation *= new Angles( 0, 0, roll );
}
void PlayerController.IEvents.OnLanded( float distance, Vector3 impactVelocity )
{
IPlayerEvent.PostToGameObject( GameObject, x => x.OnLand( distance, impactVelocity ) );
var player = Components.Get<Player>();
if ( Controller.ThirdPerson || !player.IsLocalPlayer ) return;
new Punch( new Vector3( 0.3f * distance, Random.Shared.Float( -1, 1 ), Random.Shared.Float( -1, 1 ) ), 1.0f, 1.5f, 0.7f );
}
bool noPickupNotices = false;
public IDisposable NoNoticeScope()
{
noPickupNotices = true;
return new Sandbox.Utility.DisposeAction( () => noPickupNotices = false );
}
public void ShowNotice( string message )
{
if ( noPickupNotices ) return;
NotifyNotice( message );
}
[Rpc.Owner]
public void NotifyNotice( string message )
{
if ( !IsLocalPlayer ) return;
Log.Info( $"you picked up {message}" );
Scene.RunEvent<Notices>( x => x.Display( message ) );
}
void PlayerController.IEvents.OnJumped()
{
IPlayerEvent.PostToGameObject( GameObject, x => x.OnJump() );
var player = Components.Get<Player>();
if ( Controller.ThirdPerson || !player.IsLocalPlayer ) return;
new Punch( new Vector3( -20, 0, 0 ), 0.5f, 2.0f, 1.0f );
}
}