Park/Events/EventManager.cs
using System;
using HC3;
using System.Text.Json.Serialization;

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

	/// <summary>
	/// The probability of an event happening. This is a value between 0 and 1.
	/// </summary>
	[Property, Range( 0, 1 )] float EventProbability { get; set; } = 0.1f;
	/// <summary>
	/// The time it takes for the event to happen.
	/// </summary>
	[Property] RangedFloat EventTime { get; set; } = new( 0.0f, 10.0f );

	[Property, ReadOnly, JsonIgnore]
	private float nextEventDelay;

	[Property, ReadOnly, JsonIgnore]
	private TimeSince timeSinceLastEvent;

	[Property, ReadOnly, JsonIgnore]
	public List<GameObject> ActiveEvents { get; set; } = new();

	public static IEnumerable<GameObject> AllPrefabs
	{
		get
		{
			return ResourceLibrary.GetAll<PrefabFile>()
				.Where( x => x.GetMetadata( "Type", "" ).Equals( "Events" ) )
				.ToList()
				.Select( x => GameObject.GetPrefab( x.ResourcePath ) );
		}
	}

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

	protected override void OnStart()
	{
		timeSinceLastEvent = 0;
		SetNextEventDelay();
	}

	protected override void OnUpdate()
	{
		// Don't trigger events if we aren't the host.
		if ( !Networking.IsHost )
			return;

		if ( timeSinceLastEvent < nextEventDelay )
			return;

		timeSinceLastEvent = 0;
		SetNextEventDelay();

		if ( ShouldEventHappen() )
		{
			TriggerRandomEvent();
		}
	}
	private void SetNextEventDelay()
	{
		nextEventDelay = Random.Shared.Float( EventTime.Min, EventTime.Max );
	}

	private bool ShouldEventHappen()
	{
		var randomValue = Random.Shared.Float( 0.0f, 1.0f );
		return randomValue <= EventProbability;
	}

	private void TriggerRandomEvent()
	{
		var validEvents = AllPrefabs
			.Select( prefab => new
			{
				Prefab = prefab,
				EventComponent = prefab.GetComponent<BaseEvent>()
			} )
			.Where( x => x.Prefab.IsValid() && x.EventComponent.IsValid() )
			// Only keep events that aren't already active (match by Title)
			.Where( x => !ActiveEvents.Any( active =>
			{
				var activeEvent = active.GetComponent<BaseEvent>();
				return activeEvent != null && activeEvent.Title == x.EventComponent.Title;
			} ) )
			.ToList();

		if ( validEvents.Count == 0 )
		{
			return;
		}

		// Calculate total weight from all event probabilities
		float totalWeight = validEvents.Sum( x => x.EventComponent.Probability );
		if ( totalWeight <= 0 )
		{
			Log.Warning( "[EventManager] All events have 0 probability." );
			return;
		}

		// Pick a random event based on weighted probabilities
		float randomValue = Random.Shared.Float( 0, totalWeight );
		float cumulative = 0;

		foreach ( var entry in validEvents )
		{
			cumulative += entry.EventComponent.Probability;
			if ( randomValue <= cumulative )
			{
				var newEvent = entry.Prefab.Clone();
				newEvent.Parent = GameObject;

				var instance = newEvent.GetComponent<BaseEvent>();
				newEvent.NetworkSpawn();

				NotificationPanel.Instance?.Broadcast( $"{instance.Title} has started!", "event" );
				Log.Info( $"[EventManager] Triggered event: {instance.Title}" );

				Stats.Increment( "event.triggered" );
				Stats.Increment( $"event.triggered.{instance.Title.ToIdentifier()}" );

				return;
			}
		}

		Log.Error( "[EventManager] Failed to select an event by probability." );
	}
}