Player/Player.Lifecycle.cs

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.

Networking
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 );
	}
}