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