Player.cs
using System;
using System.Linq;
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Diagnostics;
using Sandbox.Services;
namespace Facepunch.BombRoyale;
public class Player : Component, IHealthComponent, Component.ICollisionListener
{
public static Player Me { get; private set; }
[Sync( SyncFlags.FromHost )]
public LifeState LifeState { get; private set; } = LifeState.Dead;
[Sync( SyncFlags.FromHost )]
public float MaxHealth { get; private set; } = 100f;
[Sync( SyncFlags.FromHost )]
public float Health { get; private set; }
[Sync( SyncFlags.FromHost )]
public int PlayerSlot { get; set; }
[Sync( SyncFlags.FromHost )]
public TimeSince LastTakeDamageTime { get; private set; }
[Sync( SyncFlags.FromHost )]
public DiseaseType Disease { get; set; } = DiseaseType.None;
[Sync( SyncFlags.FromHost )]
public TimeUntil RemoveDiseaseTime { get; set; }
[Sync( SyncFlags.FromHost )]
public bool HasSuperBomb { get; set; }
[Sync( SyncFlags.FromHost )]
public int SpeedBoosts { get; set; }
[Sync( SyncFlags.FromHost )]
public int LivesLeft { get; set; }
[Sync( SyncFlags.FromHost )]
public int BombRange { get; set; }
[Sync( SyncFlags.FromHost )]
public int MaxBombs { get; set; }
[Sync( SyncFlags.FromHost )] private Guid HoldingBombId { get; set; }
public Bomb HoldingBomb => Scene.Directory.FindComponentByGuid( HoldingBombId ) as Bomb;
private TimeUntil NextRandomTeleport { get; set; }
private TimeUntil NextRandomBomb { get; set; }
private DiseaseSprite DiseaseSprite { get; set; }
private Vector2 InputDirection { get; set; }
[Sync] private Vector3 WishVelocity { get; set; }
[Property] public CitizenAnimationHelper Animation { get; set; }
[Property] public MoveController Controller { get; set; }
[Property] public SkinnedModelRenderer Renderer { get; set; }
[Property] public RagdollController Ragdoll { get; set; }
[Property] public GameObject BombPrefab { get; set; }
public int ConsecutiveWins { get; set; }
public int EnemiesKilled { get; set; }
private static readonly Color[] Colors =
[
"#F6D953",
"#DB3D76",
"#3DBFDB",
"#FF881B"
];
private readonly Vector3[] Cardinals =
[
Vector3.Forward,
Vector3.Left,
Vector3.Right,
Vector3.Backward
];
public Color GetTeamColor()
{
return Colors[PlayerSlot];
}
[Rpc.Broadcast( NetFlags.HostOnly )]
public void Respawn()
{
if ( Networking.IsHost )
{
Ragdoll.Unragdoll();
LifeState = LifeState.Alive;
EnemiesKilled = 0;
SpeedBoosts = 0;
LivesLeft = 1;
BombRange = 2;
Disease = DiseaseType.None;
MaxBombs = 1;
Health = MaxHealth;
}
if ( !IsProxy )
{
Controller.Velocity = Vector3.Zero;
MoveToSpawnpoint();
ShowRespawnEffect( WorldPosition );
}
}
[Rpc.Owner]
public void IncrementStat( string statName, int amount = 1 )
{
Stats.Increment( statName, amount );
}
[Rpc.Owner]
public void UnlockAchievement( string achievementName )
{
Achievements.Unlock( achievementName );
}
public bool IsInsideBomb()
{
var trace = Scene.Trace.Ray( WorldPosition, WorldPosition )
.Size( Controller.BoundingBox.Size )
.IgnoreGameObject( GameObject )
.WithTag( "bomb" )
.Run();
return trace.GameObject?.Components.Get<Bomb>() is not null;
}
public bool IsInsideBomb( Bomb bomb )
{
var trace = Scene.Trace.Ray( WorldPosition, WorldPosition )
.Size( Controller.BoundingBox.Size )
.IgnoreGameObject( GameObject )
.WithTag( "bomb" )
.Run();
return trace.GameObject.IsValid() && trace.GameObject.Components.Get<Bomb>() == bomb;
}
public void SetHoldingBomb( Bomb bomb )
{
Assert.True( Networking.IsHost );
HoldingBombId = bomb.Id;
}
private void MoveToSpawnpoint()
{
var spawnpoints = Scene.GetAllComponents<PlayerSpawn>().ToList();
spawnpoints.Sort( ( a, b ) => a.Index.CompareTo( b.Index ) );
var spawnpoint = spawnpoints[PlayerSlot];
if ( !spawnpoint.IsValid() )
throw new( $"Can't find spawnpoint for player slot #{PlayerSlot}" );
WorldPosition = spawnpoint.WorldPosition;
WorldRotation = spawnpoint.WorldRotation;
}
public void GiveDisease( DiseaseType disease )
{
Assert.True( Networking.IsHost );
RemoveDiseaseTime = Game.Random.Float( 10f, 20f );
Disease = disease;
if ( disease == DiseaseType.RandomBomb )
NextRandomBomb = Game.Random.Float( 1f, 2f );
else if ( disease == DiseaseType.Teleport )
NextRandomTeleport = Game.Random.Float( 0.5f, 1f );
Chat.AddPlayerEvent( "infected", Network.Owner.DisplayName, GetTeamColor(), $"has been infected with {disease.GetName()}" );
}
public int GetBombsLeft() => MaxBombs - GetPlacedBombCount();
public int GetPlacedBombCount()
{
return !Scene.IsValid() ? 0 : Scene.GetAllComponents<Bomb>().Count( b => b.Player == this && b.IsPlaced );
}
public void TakeDamage( DamageType type, float damage, Vector3 position, Vector3 force, Component attacker )
{
Assert.True( Networking.IsHost );
if ( LifeState == LifeState.Dead ) return;
if ( type != DamageType.Explosion ) return;
LastTakeDamageTime = 0f;
LivesLeft--;
if ( LivesLeft <= 0 )
{
using ( Rpc.FilterInclude( Network.Owner ) )
{
PlaySound( "player.die" );
}
LifeState = LifeState.Dead;
var direction = Vector3.Up + new Vector3( Game.Random.Float( -0.25f, 0.25f ), Game.Random.Float( -0.25f, 0.25f ), 0f );
Ragdoll.Ragdoll( position, direction );
var deathMessage = "has been blown to smithereens!";
if ( attacker is Player attackingPlayer )
{
attackingPlayer.EnemiesKilled++;
deathMessage = $"has been blown to smithereens by {attackingPlayer.Network.Owner.DisplayName}!";
if ( attackingPlayer.EnemiesKilled == 3 )
attackingPlayer.UnlockAchievement( "ace_3x" );
}
Chat.AddPlayerEvent( "death", Network.Owner.DisplayName, GetTeamColor(), deathMessage );
}
else
{
using ( Rpc.FilterInclude( Network.Owner ) )
{
PlaySound( "lose.life" );
}
}
}
protected override void OnStart()
{
if ( Network.IsOwner ) Me = this;
if ( !Networking.IsHost )
{
BombRoyale.AddPlayer( PlayerSlot, this );
}
base.OnStart();
}
protected override void OnDestroy()
{
if ( DiseaseSprite.IsValid() )
{
DiseaseSprite.GameObject.Destroy();
DiseaseSprite = null;
}
base.OnDestroy();
}
protected override void OnFixedUpdate()
{
if ( !IsProxy )
{
if ( !BombRoyale.IsPaused )
{
UpdateDamageScale();
if ( LifeState == LifeState.Alive )
{
UpdateMovement();
if ( Input.Released( "attack1" ) )
{
PlaceBombOnHost();
}
}
}
else
{
Controller.Velocity = 0f;
}
}
if ( Networking.IsHost )
{
if ( Disease > DiseaseType.None && RemoveDiseaseTime )
{
Disease = DiseaseType.None;
}
if ( LifeState == LifeState.Alive )
{
UpdateDiseaseEffects();
}
}
// Conna: Just because Rider said I could do this I did it for the banter. What the fuck.
switch ( Disease )
{
case DiseaseType.None when DiseaseSprite.IsValid():
DiseaseSprite.GameObject.Destroy();
DiseaseSprite = null;
return;
case > DiseaseType.None when !DiseaseSprite.IsValid():
DiseaseSprite = DiseaseSprite.Create( this );
break;
}
}
protected override void OnUpdate()
{
UpdateLifeState( LifeState );
if ( LifeState == LifeState.Alive )
UpdateAnimation();
if ( IsProxy ) return;
UpdateCamera();
}
private void UpdateDiseaseEffects()
{
if ( BombRoyale.IsPaused )
return;
if ( Disease == DiseaseType.RandomBomb && NextRandomBomb )
{
NextRandomBomb = Game.Random.Float( 1f, 2f );
if ( !IsInsideBomb() && GetBombsLeft() > 0 )
{
IncrementStat( "bombs_pooped" );
PlaySound( "disease.poop" );
var bombGo = BombPrefab.Clone();
var bomb = bombGo.Components.Get<Bomb>();
bomb.Place( this );
bombGo.NetworkSpawn();
}
}
else if ( Disease == DiseaseType.Teleport && NextRandomTeleport )
{
NextRandomTeleport = Game.Random.Float( 4f, 8f );
var randomPlayer = Scene.GetAllComponents<Player>()
.Where( p => !p.Equals( this ) )
.Shuffle()
.FirstOrDefault();
if (!randomPlayer.IsValid())
return;
var a = randomPlayer.WorldPosition;
var b = WorldPosition;
randomPlayer.DisableDiseaseSpreader( 1f );
randomPlayer.Teleport( b );
randomPlayer.ShowRespawnEffect( b );
DisableDiseaseSpreader( 1f );
Teleport( a );
ShowRespawnEffect( a );
}
}
public void DisableDiseaseSpreader( float duration )
{
var spreader = Components.GetInDescendants<DiseaseSpreader>();
if ( !spreader.IsValid() ) return;
spreader.IsActive = duration;
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void Teleport( Vector3 position )
{
if ( IsProxy ) return;
WorldPosition = position;
}
private void UpdateDamageScale()
{
var tx = Transform.Local;
if ( LastTakeDamageTime < 2f )
{
var fraction = 1f - (LastTakeDamageTime / 2f);
tx.Scale = 1f + (0.2f * MathF.Sin( Time.Now * 20f )) * fraction;
}
else
{
tx.Scale = tx.Scale.LerpTo( 1f, Time.Delta * 4f );
}
Transform.Local = tx;
}
[Rpc.Broadcast( NetFlags.OwnerOnly )]
private void PlaceBombOnHost()
{
if ( !Networking.IsHost )
return;
if ( IsInsideBomb() )
return;
if ( HoldingBomb.IsValid() )
{
PlaySound( "bomb.place" );
HoldingBomb.Place( this );
HoldingBombId = default;
}
else if ( GetBombsLeft() > 0 )
{
var bombGo = BombPrefab.Clone();
var bomb = bombGo.Components.Get<Bomb>();
bomb.Place( this );
bombGo.NetworkSpawn();
PlaySound( "bomb.place" );
}
else
{
using ( Rpc.FilterInclude( Network.Owner ) )
{
PlaySound( "bomb.nobomb" );
}
}
}
[Rpc.Broadcast]
private void PlaySound( string soundName )
{
Sound.Play( soundName, WorldPosition );
}
private void UpdateLifeState( LifeState state )
{
var isAlive = state == LifeState.Alive;
Controller.Enabled = isAlive;
Renderer.Enabled = Ragdoll.IsRagdolled || isAlive;
var children = Components.GetAll<ModelRenderer>( FindMode.EverythingInDescendants );
foreach ( var child in children )
{
child.Enabled = Ragdoll.IsRagdolled || isAlive;
}
}
[Rpc.Broadcast]
private void ShowRespawnEffect( Vector3 position )
{
RespawnEffect.Create( Scene, position, GetTeamColor() );
Sound.Play( "player.teleport", position );
}
private void UpdateAnimation()
{
var animator = Animation;
animator.HoldType = CitizenAnimationHelper.HoldTypes.None;
animator.WithVelocity( Controller.Velocity );
animator.WithWishVelocity( WishVelocity );
animator.IsGrounded = true;
animator.MoveRotationSpeed = 0f;
animator.DuckLevel = 0f;
animator.MoveStyle = CitizenAnimationHelper.MoveStyles.Run;
}
private Vector3 SnapInputDirection( Vector3 direction )
{
if ( direction.Length == 0f )
return direction;
var output = Vector3.Zero;
var ldp = -1f;
foreach ( var c in Cardinals )
{
var dp = direction.Dot( c );
if ( dp > ldp )
{
output = c;
ldp = dp;
}
}
return output;
}
private float GetWishSpeed()
{
const float walkSpeed = 150f;
return Disease switch
{
DiseaseType.MoveFast => walkSpeed * 1.75f,
DiseaseType.MoveSlow => walkSpeed * 0.75f,
_ => walkSpeed + (25f * SpeedBoosts)
};
}
private void UpdateMovement()
{
InputDirection = SnapInputDirection( Input.AnalogMove );
WishVelocity = new( InputDirection.x, InputDirection.y, 0f );
WishVelocity = WishVelocity.WithZ( 0f );
WishVelocity *= GetWishSpeed();
Controller.Velocity = Controller.Velocity.WithZ( 0f );
Controller.Accelerate( WishVelocity );
Controller.ApplyFriction( 4f );
if ( InputDirection.Length > 0f )
{
ApplyLaneAssist();
}
var previousZ = WorldPosition.z;
Controller.Move();
WorldPosition = WorldPosition.WithZ( previousZ );
if ( InputDirection.Length > 0f )
{
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( InputDirection, Vector3.Up ),
Time.Delta * 16f );
}
}
private Vector3 PreviousInputDirection { get; set; }
private void ApplyLaneAssist()
{
const float gridSize = 32f;
const float assistStrength = 12f;
const float maxOffset = 14f;
var pos = WorldPosition;
var movingX = MathF.Abs( InputDirection.x ) > 0.5f;
var movingY = MathF.Abs( InputDirection.y ) > 0.5f;
if ( movingX )
{
var laneY = GetBestLane( pos.y, PreviousInputDirection.y, gridSize );
var offset = laneY - pos.y;
if ( MathF.Abs( offset ) <= maxOffset )
Controller.Velocity = Controller.Velocity.WithY( offset * assistStrength );
}
else if ( movingY )
{
var laneX = GetBestLane( pos.x, PreviousInputDirection.x, gridSize );
var offset = laneX - pos.x;
if ( MathF.Abs( offset ) <= maxOffset )
Controller.Velocity = Controller.Velocity.WithX( offset * assistStrength );
}
PreviousInputDirection = InputDirection;
}
private static float GetBestLane( float position, float previousAxisInput, float gridSize )
{
var lower = MathF.Floor( position / gridSize ) * gridSize;
var upper = lower + gridSize;
if ( MathF.Abs( position - lower ) < 0.5f )
return lower;
if ( MathF.Abs( position - upper ) < 0.5f )
return upper;
if ( previousAxisInput > 0.1f )
return upper;
if ( previousAxisInput < -0.1f )
return lower;
return ( position - lower ) < ( upper - position ) ? lower : upper;
}
private void UpdateCamera()
{
var boundsComponent = Scene.GetAllComponents<ArenaBounds>().FirstOrDefault();
var worldBounds = boundsComponent.Bounds;
var camera = Scene.Camera;
var totalHeight = worldBounds.Size.Length;
camera.WorldPosition = worldBounds.Center + Vector3.Up * totalHeight * 0.85f + Vector3.Backward * totalHeight * 0.15f;
var direction = (worldBounds.Center - camera.WorldPosition).Normal - Vector3.Backward *.002f;
camera.WorldRotation = Rotation.LookAt( direction );
camera.FieldOfView = Screen.CreateVerticalFieldOfView( 60f );
ScreenShake.Apply( camera );
Sound.Listener = new( WorldPosition + Vector3.Up * 60f, Rotation.LookAt( Vector3.Forward ) );
}
}