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