MissionPlayerState.cs
using Sandbox;
using SWB.Player;
using System.Linq;
// Owns "am I still in this job?" for one player. Put this on the player
// prefab alongside PlayerBase.
//
// During a mission there are no respawns: going down puts you out for the
// rest of the run, watching through a teammate's eyes. Escaping does the
// same thing minus the dying. Once nobody is left in the fight, MissionTimer
// wraps up and sends everyone back to the hideout.
//
// Outside a mission (i.e. in the hideout) this does nothing and normal
// SWB respawning is left alone.
public sealed class MissionPlayerState : Component
{
public enum State
{
/// <summary>Still in the fight.</summary>
Active,
/// <summary>Went down. Out for the rest of the run.</summary>
Eliminated,
/// <summary>Got out with the money.</summary>
Escaped
}
public State Current { get; private set; } = State.Active;
public bool IsOut => Current != State.Active;
/// <summary>Who we're currently watching, if anyone.</summary>
public PlayerBase SpectateTarget { get; private set; }
PlayerBase _player;
CameraMovement _cameraMovement;
bool _wasAlive = true;
protected override void OnAwake()
{
_player = Components.Get<PlayerBase>();
_cameraMovement = Components.GetInChildrenOrSelf<CameraMovement>();
if ( _player is null )
Log.Warning( $"{GameObject.Name}: MissionPlayerState needs a PlayerBase on the same GameObject." );
}
protected override void OnUpdate()
{
if ( _player is null )
return;
// Runs on every client, not just the owner: someone who's out has
// to be invisible on everyone's screen, not only their own.
if ( IsOut )
HideBody();
if ( IsProxy )
return;
if ( Current == State.Active )
CheckForElimination();
else
EnforceOutOfPlay();
_wasAlive = _player.IsAlive;
}
void CheckForElimination()
{
// Only alive -> dead counts, and only during an actual job. Dying
// in the hideout (or before the job starts) falls through to SWB's
// normal respawn.
if ( _player.IsAlive || !_wasAlive )
return;
if ( !MissionInProgress() )
return;
SetOut( State.Eliminated );
}
// Called by EscapeZone when this player reaches the exit.
public void MarkEscaped()
{
SetOut( State.Escaped );
}
void SetOut( State state )
{
SetOutBroadcast( (int)state );
}
// Broadcast because every client runs its own MissionTimer and reads
// these tags to decide whether anyone is still in the fight. Applied
// on the owner only, the rest of the crew would keep counting a dead
// player as alive and the job would never end for them.
[Rpc.Broadcast]
void SetOutBroadcast( int stateValue )
{
Current = (State)stateValue;
// MissionTimer and CopBotAI both read these tags to decide who
// still counts as being in the fight.
GameObject.Tags.Add( Current == State.Escaped ? "escaped" : "eliminated" );
// Only our own view needs handing over to someone else; other
// clients have their own cameras to worry about.
if ( !IsProxy && _cameraMovement is not null )
_cameraMovement.Enabled = false;
Log.Info( $"{GameObject.Name} is out of the run: {Current}." );
}
// Runs every frame rather than once, because PlayerBase.OnDeath fires
// a 2-second delayed respawn that we can't call off - it'll reset
// health and teleport the body to a spawn point regardless. Rather
// than fighting that, we let it happen and just keep re-applying the
// out-of-play state on top, which is far less fragile than trying to
// intercept an async respawn.
void EnforceOutOfPlay()
{
// Can't be hurt, and can't die a second time (which would
// otherwise cost an escaped player their loadout).
_player.GodMode = true;
UpdateSpectate();
}
// Invisible, and no hitbox for cops to shoot at.
//
// Only the body capsule, deliberately - blanket-disabling every
// collider would take the CharacterController's out from under
// PlayerBase.Move(), which still runs every tick on a player who's
// out. GodMode is what actually makes them unkillable; this is so
// cops' line-of-sight traces don't stop on an invisible body.
void HideBody()
{
if ( _player.BodyRenderer.IsValid() )
_player.BodyRenderer.Enabled = false;
if ( _player.BodyCollider.IsValid() )
_player.BodyCollider.Enabled = false;
}
void UpdateSpectate()
{
// Target can die while we're watching them, so re-check rather
// than holding onto a corpse.
if ( !IsValidTarget( SpectateTarget ) )
SpectateTarget = FindSpectateTarget();
if ( SpectateTarget is null || !_player.Camera.IsValid() )
return;
// Sit in their head and look where they're looking. CameraMovement
// was switched off in SetOut, so nothing fights us for the camera
// transform.
_player.Camera.WorldPosition = SpectateTarget.EyePos;
_player.Camera.WorldRotation = SpectateTarget.EyeAngles.ToRotation();
}
bool IsValidTarget( PlayerBase target )
{
if ( !target.IsValid() )
return false;
var go = target.GameObject;
return target.IsAlive
&& !go.Tags.Has( "eliminated" )
&& !go.Tags.Has( "escaped" );
}
PlayerBase FindSpectateTarget()
{
return Scene.GetAllComponents<PlayerBase>()
.FirstOrDefault( p => p != _player && !p.IsBot && IsValidTarget( p ) );
}
static bool MissionInProgress()
{
var timer = Game.ActiveScene?.GetAllComponents<MissionTimer>().FirstOrDefault();
return timer?.MissionInProgress ?? false;
}
}