GameManager.cs
using Sandbox;
using Sandbox.Network;
using System.Linq;

public sealed class GameManager : Component, Component.INetworkListener
{
	[Property, Category( "Settings" )] 
	public GameObject PlayerPrefab { get; set; }

	[Property, Category( "Round" ), Sync] public float TimeRemaining { get; set; } = 120f;
	[Property, Category( "Round" ), Sync] public bool IsRoundActive { get; set; } = true;
	[Property, Category( "Round" ), Sync] public int WinnerTeamId { get; set; } = -1; // -1 = Active, 0 = Blue, 1 = Red, 2 = Draw
	[Property, Category( "Round" ), Sync] public int BluePaintCount { get; set; } = 0;
	[Property, Category( "Round" ), Sync] public int RedPaintCount { get; set; } = 0;
	[Property, Category( "Round" ), Sync] public int TargetPaintCapacity { get; set; } = 300;

	private TimeSince timeSinceRoundEnded;

	protected override void OnStart()
	{
		// Очищаємо залишки слизу з попереднього раунду
		PaintProjectile.PaintGrid.Clear();

		// Розраховуємо місткість слизу динамічно на основі розмірів карти
		CalculateDynamicMapCapacity();

		// Якщо гравець запускає сцену в соло-тесті в редакторі без підключення до мережі
		if ( !Networking.IsActive )
		{
			// Перевіряємо, чи вже є гравець на сцені
			var existingPlayer = Scene.GetAllComponents<PlayerController>().FirstOrDefault();
			if ( existingPlayer.IsValid() )
			{
				Log.Info( "Local player already exists in the scene. Skipping spawn." );
				
				// Переконуємось, що існуючий гравець отримав команду та кольори
				var teamComponent = existingPlayer.GameObject.Components.Get<PlayerTeam>() ?? existingPlayer.GameObject.Components.GetInDescendants<PlayerTeam>();
				if ( !teamComponent.IsValid() )
				{
					teamComponent = existingPlayer.Components.Create<PlayerTeam>();
				}
				teamComponent.TeamId = Random.Shared.Int( 0, 1 );
				return;
			}

			Log.Info( "Solo play detected. Spawning local player..." );
			SpawnPlayerForConnection( null );
		}
	}

	/// <summary>
	/// Викликається на хості, коли новий клієнт повністю підключився до гри
	/// </summary>
	public void OnActive( Connection connection )
	{
		Log.Info( $"Player fully connected: {connection.DisplayName}" );
		SpawnPlayerForConnection( connection );
	}

	private void SpawnPlayerForConnection( Connection connection )
	{
		if ( !PlayerPrefab.IsValid() )
		{
			Log.Error( "PlayerPrefab is not assigned in GameManager!" );
			return;
		}

		// 1. Визначаємо команду гравця (0 - Blue, 1 - Red)
		// Зроблено рандомним для зручності тестування обох команд.
		int teamId = Random.Shared.Int( 0, 1 );

		Log.Info( $"Assigning {(connection != null ? connection.DisplayName : "Local Player")} to Team {teamId}" );

		// 2. Шукаємо відповідну точку спавну на карті
		global::Transform spawnTransform = global::Transform.Zero;
		var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToList();

		if ( spawnPoints.Count > 0 )
		{
			// Шукаємо точки спавну з тегом нашої команди ("team0" або "team1")
			string teamTag = $"team{teamId}";
			var teamSpawns = spawnPoints.Where( x => x.Tags.Has( teamTag ) ).ToList();

			if ( teamSpawns.Count > 0 )
			{
				var chosenSpawn = teamSpawns[Random.Shared.Int( 0, teamSpawns.Count - 1 )];
				spawnTransform = chosenSpawn.Transform.World;
			}
			else
			{
				// Якщо командних спавнів немає, беремо будь-який
				var chosenSpawn = spawnPoints[Random.Shared.Int( 0, spawnPoints.Count - 1 )];
				spawnTransform = chosenSpawn.Transform.World;
			}
		}
		else
		{
			Log.Warning( "No SpawnPoints found in the scene! Spawning at world origin (0, 0, 0)." );
		}

		// 3. Створюємо копію префабу персонажа гравця
		var playerInstance = PlayerPrefab.Clone( spawnTransform );

		// 4. Шукаємо компонент команди в ієрархії гравця (де б він не був — на корні чи в GameHud)
		var teamComponent = playerInstance.Components.Get<PlayerTeam>() ?? playerInstance.Components.GetInDescendants<PlayerTeam>();
		if ( !teamComponent.IsValid() )
		{
			teamComponent = playerInstance.Components.Create<PlayerTeam>();
		}
		teamComponent.TeamId = teamId;
		teamComponent.ApplyTeamColor();

		// 5. Спавнимо об'єкт у мережі та призначаємо володіння з'єднанню клієнта
		if ( connection != null )
		{
			playerInstance.NetworkSpawn( connection );
		}
		
		Log.Info( $"Player spawned successfully for Team {teamId}!" );
	}

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

		if ( IsRoundActive )
		{
			// Зменшуємо таймер
			TimeRemaining = MathF.Max( 0f, TimeRemaining - Time.Delta );

			// Підраховуємо плями слизу для кожної команди (враховуючи розмір плям)
			float blueScore = 0f;
			float redScore = 0f;

			foreach ( var kvp in PaintProjectile.PaintGrid )
			{
				if ( kvp.Value.IsValid() )
				{
					var blob = kvp.Value.Components.Get<PaintBlob>();
					if ( blob.IsValid() )
					{
						// Вага плями слизу дорівнює її горизонтальному масштабу (розміру/площі)
						float weight = kvp.Value.LocalScale.x;
						if ( blob.TeamId == 0 ) blueScore += weight;
						else if ( blob.TeamId == 1 ) redScore += weight;
					}
				}
			}

			BluePaintCount = (int)MathF.Round( blueScore );
			RedPaintCount = (int)MathF.Round( redScore );

			if ( TimeRemaining <= 0f )
			{
				EndRound();
			}
		}
		else
		{
			// Якщо раунд закінчено, чекаємо 8 секунд і перезапускаємо карту
			if ( timeSinceRoundEnded >= 8f )
			{
				RestartRound();
			}
		}
	}

	private void EndRound()
	{
		IsRoundActive = false;
		timeSinceRoundEnded = 0f;

		// Визначаємо переможця
		if ( BluePaintCount > RedPaintCount )
		{
			WinnerTeamId = 0; // Blue Wins
		}
		else if ( RedPaintCount > BluePaintCount )
		{
			WinnerTeamId = 1; // Red Wins
		}
		else
		{
			WinnerTeamId = 2; // Draw
		}

		Log.Info( $"Round ended! Winner: {WinnerTeamId}. Blue: {BluePaintCount}, Red: {RedPaintCount}" );
	}

	private void RestartRound()
	{
		Log.Info( "Restarting round..." );
		// Перезавантажуємо поточну сцену
		if ( Scene.Source != null )
		{
			Scene.Load( Scene.Source );
		}
	}

	private void CalculateDynamicMapCapacity()
	{
		var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToList();
		if ( spawnPoints.Count > 1 )
		{
			float minX = spawnPoints.Min( p => p.WorldPosition.x );
			float maxX = spawnPoints.Max( p => p.WorldPosition.x );
			float minY = spawnPoints.Min( p => p.WorldPosition.y );
			float maxY = spawnPoints.Max( p => p.WorldPosition.y );
			
			float width = maxX - minX;
			float height = maxY - minY;
			
			// Розрахунок площі карти та підбір місткості
			float mapArea = MathF.Max( 100000f, width * height );
			
			// Кожна пляма слизу масштабу ~1.0 має площу близько 2000 юнітів.
			// Розраховуємо місткість так, щоб раунд був насиченим
			float estimatedCapacity = ( mapArea / 2000f ) * 0.05f; 
			
			TargetPaintCapacity = (int)MathF.Max( 150f, MathF.Min( 1200f, estimatedCapacity ) );
			Log.Info( $"Dynamic Map Capacity calculated: {TargetPaintCapacity} (Map area estimated from {spawnPoints.Count} spawn points: {width}x{height})" );
		}
		else
		{
			// Дефолтна місткість, якщо мало точок спавну
			TargetPaintCapacity = 300;
			Log.Info( $"Using default paint capacity: {TargetPaintCapacity}" );
		}
	}
}