RoundMutatorManager.cs

RoundMutatorManager is a scene component that rolls a random round mutator (thick fog, low gravity, overclocked, chaos anvils) and applies its effects. Fog disables scene lights, low gravity adds upward velocity to character controllers, chaos anvils periodically drops anvils or explosive barrels near a living player. Also carries the banner timer and the localized title, description and icon strings for each mutator.

🐞 ApplyMutatorEffects only runs where RollMutator is called, so the synced ActiveMutator never disables DirectionalLight or PointLight on proxies and clients stay fully lit during ThickFog.
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public enum RoundMutatorType
{
	None,
	ThickFog,
	LowGravity,
	Overclocked,
	ChaosAnvils
}

public sealed class RoundMutatorManager : Component
{
	public static RoundMutatorManager Instance { get; private set; }

	[Sync] public RoundMutatorType ActiveMutator { get; private set; } = RoundMutatorType.None;
	[Sync] public float BannerRemaining { get; private set; } = 0f;
	[Property] public float BannerDuration { get; set; } = 8f;

	[Property] public GameObject HeavyAnvilPrefab { get; set; }
	[Property] public GameObject ExplosiveBarrelPrefab { get; set; }

	[Sync] private TimeSince _timeSinceChaosDrop { get; set; } = 0f;
	private readonly List<Component> _dimmedLights = new();
	private bool _isFogApplied = false;

	protected override void OnAwake()
	{
		Instance = this;
	}

	protected override void OnStart()
	{
		Instance = this;
	}

	public void RollMutator()
	{
		ResetMutator();

		// 25% chance None, 75% chance one of the 4 mutators
		float roll = Game.Random.Float( 0f, 1f );
		if ( roll < 0.25f )
		{
			ActiveMutator = RoundMutatorType.None;
			BannerRemaining = 0f;
			Log.Info( "[RoundMutatorManager] Standard Round (No Mutator)." );
			return;
		}

		var mutators = new[]
		{
			RoundMutatorType.ThickFog,
			RoundMutatorType.LowGravity,
			RoundMutatorType.Overclocked,
			RoundMutatorType.ChaosAnvils
		};

		ActiveMutator = mutators[Game.Random.Int( 0, mutators.Length - 1 )];
		BannerRemaining = BannerDuration;
		_timeSinceChaosDrop = 0f;

		ApplyMutatorEffects();
		Log.Info( $"[RoundMutatorManager] Active Mutator chosen: {ActiveMutator}" );
	}

	public void ResetMutator()
	{
		if ( ActiveMutator == RoundMutatorType.None ) return;

		RemoveMutatorEffects();
		ActiveMutator = RoundMutatorType.None;
		BannerRemaining = 0f;
	}

	private void ApplyMutatorEffects()
	{
		switch ( ActiveMutator )
		{
			case RoundMutatorType.ThickFog:
				ApplyThickFog( true );
				break;

			case RoundMutatorType.LowGravity:
				// Applied dynamically in OnUpdate
				break;

			case RoundMutatorType.Overclocked:
				// Speed bonus to Nextbots applied in NextbotAI and 2x revive in PlayerHealth
				break;

			case RoundMutatorType.ChaosAnvils:
				_timeSinceChaosDrop = 0f;
				break;
		}
	}

	private void RemoveMutatorEffects()
	{
		if ( _isFogApplied )
		{
			ApplyThickFog( false );
		}
	}

	private void ApplyThickFog( bool enable )
	{
		_isFogApplied = enable;
		if ( enable )
		{
			_dimmedLights.Clear();
			foreach ( var dl in Scene.GetAllComponents<DirectionalLight>() )
			{
				if ( dl.Enabled )
				{
					dl.Enabled = false;
					_dimmedLights.Add( dl );
				}
			}
			foreach ( var pl in Scene.GetAllComponents<PointLight>() )
			{
				if ( pl.Enabled && !pl.GameObject.Name.Contains( "Player" ) && !pl.GameObject.Name.Contains( "Flashlight" ) )
				{
					pl.Enabled = false;
					_dimmedLights.Add( pl );
				}
			}
		}
		else
		{
			foreach ( var comp in _dimmedLights )
			{
				if ( !comp.IsValid() ) continue;
				if ( comp is DirectionalLight dl ) dl.Enabled = true;
				else if ( comp is PointLight pl ) pl.Enabled = true;
			}
			_dimmedLights.Clear();
		}
	}

	protected override void OnUpdate()
	{
		if ( BannerRemaining > 0f )
		{
			BannerRemaining -= Time.Delta;
		}

		if ( IsProxy ) return;

		if ( EvadeRoundManager.Instance?.CurrentPhase != EvadePhase.Survival ) return;

		// 1. Low Gravity Mutator physics update
		if ( ActiveMutator == RoundMutatorType.LowGravity )
		{
			foreach ( var cc in Scene.GetAllComponents<CharacterController>() )
			{
				if ( !cc.IsValid() || !cc.Enabled ) continue;
				if ( !cc.IsOnGround && cc.Velocity.z < 0f )
				{
					// Float down gently (reduces downward pull by ~60%)
					cc.Velocity += Vector3.Up * ( 420f * Time.Delta );
				}
			}
		}

		// 2. Chaos Anvils Mutator periodic drop
		if ( ActiveMutator == RoundMutatorType.ChaosAnvils && _timeSinceChaosDrop >= 5.5f )
		{
			_timeSinceChaosDrop = 0f;
			SpawnChaosHazard();
		}
	}

	private void SpawnChaosHazard()
	{
		var players = Scene.GetAllComponents<PlayerHealth>()
			.Where( p => p.IsValid() && !p.IsDead )
			.ToList();

		Vector3 spawnOrigin = Vector3.Zero;
		if ( players.Count > 0 )
		{
			var p = players[Game.Random.Int( 0, players.Count - 1 )];
			spawnOrigin = p.WorldPosition;
		}

		Vector3 offset = new Vector3(
			Game.Random.Float( -350f, 350f ),
			Game.Random.Float( -350f, 350f ),
			Game.Random.Float( 450f, 650f )
		);

		Vector3 dropPos = spawnOrigin + offset;

		bool spawnAnvil = Game.Random.Float( 0f, 1f ) > 0.4f;
		if ( spawnAnvil && HeavyAnvilPrefab.IsValid() )
		{
			var anvil = HeavyAnvilPrefab.Clone( dropPos, Rotation.FromPitch( Game.Random.Float( 0, 360 ) ) );
			_ = DestroyAfter( anvil, 12f );
		}
		else if ( !spawnAnvil && ExplosiveBarrelPrefab.IsValid() )
		{
			var barrel = ExplosiveBarrelPrefab.Clone( dropPos, Rotation.Identity );
			_ = DestroyAfter( barrel, 12f );
		}
		else
		{
			// Procedural falling anvil fallback
			var fallback = Scene.CreateObject();
			fallback.Name = "ChaosHazard_Anvil";
			fallback.WorldPosition = dropPos;

			var mr = fallback.Components.Create<ModelRenderer>();
			mr.Model = Model.Load( "models/sbox_props/metal_box/metal_box.vmdl" );

			var col = fallback.Components.Create<BoxCollider>();
			col.Scale = new Vector3( 40f, 40f, 40f );

			var rb = fallback.Components.Create<Rigidbody>();
			rb.MassOverride = 450f;

			var hz = fallback.Components.Create<HazardZone>();
			hz.DamagePerTouch = 35f;

			_ = DestroyAfter( fallback, 12f );
		}
	}

	private async Task DestroyAfter( GameObject go, float delay )
	{
		await Task.DelaySeconds( delay );
		if ( go.IsValid() ) go.Destroy();
	}

	public static string GetMutatorTitle( RoundMutatorType type ) => type switch
	{
		RoundMutatorType.ThickFog => "ГУСТОЙ ТУМАН // THICK FOG",
		RoundMutatorType.LowGravity => "ЛУННАЯ ГРАВИТАЦИЯ // LOW GRAVITY",
		RoundMutatorType.Overclocked => "ОТКЛИК И ПЕРЕГРУЗКА // OVERCLOCKED",
		RoundMutatorType.ChaosAnvils => "МЕТЕОРНЫЙ ХАОС // CHAOS ANVILS",
		_ => "ОБЫЧНЫЙ РАУНД // STANDARD ROUND"
	};

	public static string GetMutatorDescription( RoundMutatorType type ) => type switch
	{
		RoundMutatorType.ThickFog => "Видимость ограничена 10м! Освещение отключено, используйте фонарик [F].",
		RoundMutatorType.LowGravity => "Гравитация снижена на 60%! Высокие прыжки и затяжной бханн.",
		RoundMutatorType.Overclocked => "Скорость некстботов +25%, но скорость спасения союзников удвоена (2x)!",
		RoundMutatorType.ChaosAnvils => "С неба периодически падают наковальни и взрывные бочки!",
		_ => "Стандартные условия выживания."
	};

	public static string GetMutatorIcon( RoundMutatorType type ) => type switch
	{
		RoundMutatorType.ThickFog => "🌫️",
		RoundMutatorType.LowGravity => "🚀",
		RoundMutatorType.Overclocked => "⚡",
		RoundMutatorType.ChaosAnvils => "⚓",
		_ => "🛡️"
	};

	protected override void OnDestroy()
	{
		RemoveMutatorEffects();
		if ( Instance == this ) Instance = null;
		base.OnDestroy();
	}
}