PlayerTeam.cs
using Sandbox;
using System;
using System.Linq;

public sealed class PlayerTeam : Component
{
	/// <summary>
	/// Індекс команди гравця: 0 (Team Blue) або 1 (Team Red)
	/// </summary>
	[Property, Sync] 
	public int TeamId { get; set; } = 0;

	/// <summary>
	/// Кольори команд. Можна налаштувати в інспекторі.
	/// </summary>
	[Property, Category( "Colors" )]
	public Color TeamBlueColor { get; set; } = new Color( 0f, 0.3f, 1f );

	[Property, Category( "Colors" )]
	public Color TeamRedColor { get; set; } = new Color( 1f, 0f, 0f );

	/// <summary>
	/// Повертає колір поточної команди
	/// </summary>
	public Color TeamColor => TeamId == 0 ? TeamBlueColor : TeamRedColor;

	[Property, Category( "Audio" )] public SoundEvent DeathSound { get; set; }

	/// <summary>
	/// Здоров'я гравця (від 0 до 100). Синхронізується по мережі.
	/// </summary>
	[Property, Category( "Stats" ), Sync]
	public float Health { get; set; } = 100f;

	/// <summary>
	/// Чи мертвий гравець наразі. Синхронізується по мережі.
	/// </summary>
	[Property, Category( "Stats" ), Sync]
	public bool IsDead { get; set; } = false;

	private TimeSince timeSinceDeath;
	public float TimeSinceDeath => timeSinceDeath;

	/// <summary>
	/// Динамічно знаходить головний GameObject гравця (з компонентом PlayerController)
	/// </summary>
	private GameObject PlayerRoot => (GameObject.Components.Get<PlayerController>() ?? GameObject.Components.GetInAncestors<PlayerController>())?.GameObject ?? GameObject;

	protected override void OnStart()
	{
		ApplyTeamColor();
	}

	protected override void OnUpdate()
	{
		// Тільки хост керує логікою смерті та респавну
		if ( !Networking.IsHost ) return;

		if ( IsDead && timeSinceDeath >= 5f )
		{
			Respawn();
		}
	}

	/// <summary>
	/// Нанесення шкоди гравцю. Має викликатися на хості або синхронізуватися.
	/// </summary>
	public void TakeDamage( float amount )
	{
		if ( !Networking.IsHost ) return; // Тільки хост реєструє попадання
		if ( IsDead ) return;

		Health = MathF.Max( 0f, Health - amount );
		Log.Info( $"Player took {amount} damage! Current health: {Health}" );

		if ( Health <= 0f )
		{
			Die();
		}
	}

	private void Die()
	{
		if ( !Networking.IsHost ) return;

		IsDead = true;
		Health = 0f;
		timeSinceDeath = 0f;

		Log.Info( "Player has DIED!" );
		
		// Розсилаємо RPC всім клієнтам, щоб вимкнути візуал та керування
		BroadcastDeathState( true );
	}

	private void Respawn()
	{
		if ( !Networking.IsHost ) return;

		IsDead = false;
		Health = 100f;

		// Знаходимо рандомну точку спавну для нашої команди
		var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToList();
		string teamTag = $"team{TeamId}";
		var teamSpawns = spawnPoints.Where( x => x.Tags.Has( teamTag ) ).ToList();

		global::Transform spawnTransform = global::Transform.Zero;
		if ( teamSpawns.Count > 0 )
		{
			spawnTransform = teamSpawns[Random.Shared.Int( 0, teamSpawns.Count - 1 )].Transform.World;
		}
		else if ( spawnPoints.Count > 0 )
		{
			spawnTransform = spawnPoints[Random.Shared.Int( 0, spawnPoints.Count - 1 )].Transform.World;
		}

		Log.Info( $"Player respawned at: {spawnTransform.Position}" );

		// Вмикаємо все назад та телепортуємо через RPC на всіх клієнтах
		BroadcastDeathState( false, spawnTransform.Position, spawnTransform.Rotation );
	}

	/// <summary>
	/// RPC-метод для синхронного приховування гравця та вимкнення його скриптів на всіх клієнтах
	/// </summary>
	[Broadcast]
	private void BroadcastDeathState( bool isDead, Vector3 spawnPos = default, Rotation spawnRot = default )
	{
		// Граємо звук смерті при вмиранні
		if ( isDead && DeathSound != null )
		{
			Sound.Play( DeathSound, WorldPosition );
		}

		// 1. Приховуємо/показуємо 3D-модель тіла
		var bodyRenderer = PlayerRoot.Components.GetAll<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants ).FirstOrDefault();
		if ( bodyRenderer.IsValid() )
		{
			bodyRenderer.GameObject.Enabled = !isDead;
		}

		// 2. Скидаємо фізику та телепортуємо при респавні
		var rigidbody = PlayerRoot.Components.Get<Rigidbody>();
		if ( rigidbody.IsValid() )
		{
			// Ми більше не вимикаємо Rigidbody, оскільки це ламає PlayerController
			if ( !isDead )
			{
				// Скидаємо швидкості, щоб гравець не вилітав зі спавну зі старою швидкістю
				rigidbody.Velocity = Vector3.Zero;
				rigidbody.AngularVelocity = Vector3.Zero;
			}
		}

		// Телепортуємо локально на кожному клієнті під час респавну (важливо для власника з авторизацією)
		if ( !isDead )
		{
			PlayerRoot.WorldPosition = spawnPos;
			PlayerRoot.WorldRotation = spawnRot;
		}

		// 3. Блокуємо/розблоковуємо вхідне керування та камеру
		// Використовуємо UseInputControls, щоб гравець не міг бігати/стрибати мертвим,
		// але зберігаємо UseCameraControls увімкненим, щоб камера не зависала/замерзала в одній точці.
		var controller = PlayerRoot.Components.Get<PlayerController>();
		if ( controller.IsValid() )
		{
			controller.UseInputControls = !isDead;
		}

		// 4. Вимикаємо/вмикаємо зброю
		var paintGun = PlayerRoot.Components.GetAll<PaintGun>( FindMode.EverythingInSelfAndDescendants ).FirstOrDefault();
		if ( paintGun.IsValid() )
		{
			paintGun.Enabled = !isDead;
		}

		// 5. Вимикаємо/вмикаємо плавання в слизі
		var swimming = PlayerRoot.Components.GetAll<PaintSwimming>( FindMode.EverythingInSelfAndDescendants ).FirstOrDefault();
		if ( swimming.IsValid() )
		{
			swimming.Enabled = !isDead;
		}
	}

	/// <summary>
	/// Застосовує колір команди до зброї (слизу).
	/// </summary>
	public void ApplyTeamColor()
	{
		var color = TeamColor;

		// Встановлюємо колір фарби зброї (PaintGun)
		var paintGun = PlayerRoot.Components.GetAll<PaintGun>( FindMode.EverythingInSelfAndDescendants ).FirstOrDefault();
		if ( paintGun.IsValid() )
		{
			paintGun.PaintColor = color;
		}
	}
}