Player lifecycle and state properties for a Player entity. Defines synced/runtime properties like Money, Spectating, SeenTips, Mask/MaskTint and SlotColor, plus RPC methods for Respawn and Kill, money helpers, and applying mask tint to the model.
using System;
using Sandbox;
namespace BrickJam;
public sealed partial class Player
{
// Host-authoritative: every Money write (selling in SellArea, BuyUpgrade, save-load) happens on the
// host. Plain [Sync] is OWNER-authoritative, so the host's credit to a remote client's player (a proxy
// on the host) was overwritten by the owning client and never applied - clients never got paid for
// sold loot. FromHost makes the host's value the source of truth and replicates it to the owner.
[Property, Sync( SyncFlags.FromHost )] public int Money { get; set; }
[Sync] public bool SeenTips { get; set; }
/// <summary>
/// Host-driven: while true the owning client yields its first-person camera to a spectator pawn
/// (set by <see cref="MansionGame"/> when the player is dead / a late joiner).
/// </summary>
[Sync( SyncFlags.FromHost )] public bool Spectating { get; set; }
/// <summary>The mask model child; tinted to the player's slot colour.</summary>
[Property] public SkinnedModelRenderer Mask { get; set; }
[Sync] public Color MaskTint { get; set; } = Color.White;
/// <summary>The player's slot colour, resolved from the session manager via the owning connection.</summary>
public Color SlotColor => MansionGame.Instance.IsValid()
? MansionGame.Instance.GetColor( Network.Owner )
: Color.Gray;
/// <summary>
/// Respawn the owning client's player at <paramref name="spawn"/>. The spawn is computed HOST-side and
/// passed in: each client's <see cref="MansionGame.CurrentLevelType"/> may not have replicated yet when
/// this runs, so a client-computed spawn would land in the wrong (old) level - which is why everyone but
/// the host used to be left behind on a level change.
/// </summary>
[Rpc.Owner]
public void Respawn( Transform spawn )
{
WorldTransform = spawn;
IsAlive = true;
Blocked = false;
MaskTint = SlotColor;
if ( Body.IsValid() )
Body.Enabled = true;
}
/// <summary>Authority-side kill. Replaces the legacy <c>Kill()</c>.</summary>
[Rpc.Owner]
public void Kill()
{
if ( !IsAlive )
return;
IsAlive = false;
Blocked = false;
if ( Body.IsValid() )
Body.Enabled = false;
SoundExtensions.BroadcastPlay( "sounds/death.sound", WorldPosition );
MansionGame.Instance?.PlayEffect( "prefabs/particles/blood_explosion.prefab", WorldPosition + Vector3.Up * 40f, Rotation.Identity );
// Kill is [Rpc.Owner] -> runs on the victim's own client, so report the death stat directly.
GameStats.Increment( GameStats.Deaths, 1 );
// NOTE: the spectator swap is handled by MansionGame.Spectator (reconciled host-side from IsAlive).
// Legacy also did inventory.Clear() on death (lose your loot when caught) - intentionally left out
// here pending a gameplay decision; would need to run host-side since Inventory is FromHost-synced.
}
public void SetMoney( int value )
{
Money = Math.Max( value, 0 );
// TODO (deferred): broadcast a MoneyChanged event for the HUD (UI system).
}
public void AddMoney( int value ) => SetMoney( Money + value );
public void RemoveMoney( int value ) => SetMoney( Money - value );
private void ApplyMaskTint()
{
if ( Mask.IsValid() && Mask.SceneObject.IsValid() )
Mask.SceneObject.Attributes.Set( "maskTint", MaskTint );
}
}