manager/Manager.Events.cs

Manager.Events.cs, part of the Manager class, schedules and runs stage events for the game. It generates a plan of minute-indexed events, spawns events at minute boundaries, starts and tracks active StageEvent instances, and exposes methods to add, remove and restart events.

Networking
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

public enum EventType { Miniboss, Snaker, EvilChest, IcyGround, Wind, Dark, Cloudy, Lava, Mushrooms, HealingZones, Werewolf }

public partial class Manager
{
	public Dictionary<int, EventType> PlannedEvents = new();

	public Dictionary<EventType, StageEvent> ActiveEvents = new();

	public bool IsEventAnnouncementActive { get; set; }
	public const float ANNOUNCEMENT_TIME_TOTAL = 12f;
	public const float ANNOUNCEMENT_LERP_TIME = 1f;

	void GenerateEvents()
	{
		PlannedEvents.Clear();

		List<int> minutes = Enumerable.Range( 2, 10 ).ToList(); // 2-11
		HashSet<int> usedMinutes = new();

		//PlannedEvents[1] = EventType.Lava;
		//usedMinutes.Add( 1 );

		if ( Difficulty == 0 )
		{
			AddMinibossEvent( Enumerable.Range( 3, 8 ).ToList(), usedMinutes ); // 3-10
		}
		else
		{
			AddMinibossEvent( Enumerable.Range( 3, 6 ).ToList(), usedMinutes ); // 3-8
			AddMinibossEvent( Enumerable.Range( 7, 5 ).ToList(), usedMinutes ); // 7-12

			if( Difficulty == 2 )
			{
				AddMinibossEvent( Enumerable.Range( 1, 4 ).ToList(), usedMinutes ); // 1 - 4

				// Cursed: 4-6 total minibosses (already have 3 from above, add 1-3 more)
				int extraMinibosses = Game.Random.Int( 1, 3 );
				for ( int i = 0; i < extraMinibosses; i++ )
					AddMinibossEvent( Enumerable.Range( 1, 12 ).ToList(), usedMinutes ); // 1-12
			}
		}

		int numEvilChest = 0;
		switch( Difficulty )
		{
			case 0: numEvilChest = 1; break;
			case 1: numEvilChest = 2; break;
			case 2: numEvilChest = 3; break;
		}

		var availableMinutes = minutes.Except( usedMinutes ).Where( m => m >= 4 ).ToList(); // evil chest doesn't spawn until minute 4 or after

		for(int i = 0; i < numEvilChest && availableMinutes.Count > 0; i++)
		{
			int minute = availableMinutes[Game.Random.Next( availableMinutes.Count )];
			PlannedEvents[minute] = EventType.EvilChest;
			usedMinutes.Add( minute );
			availableMinutes.Remove( minute );
		}

		List<EventType> fillerPool = new() { EventType.Wind, EventType.Mushrooms, EventType.HealingZones, EventType.Cloudy, }; //  EventType.IcyGround, EventType.Snaker, EventType.Lava

		if ( Difficulty > 0 )
			fillerPool.Add( EventType.Werewolf );

		foreach ( int minute in minutes )
		{
			if ( !usedMinutes.Contains( minute ) )
			{
				if ( fillerPool.Count > 0 )
				{
					var chance = Difficulty == 2
						? Utils.Map( minute, 2, 12, 0.1f, 0.13f )
						: Utils.Map( minute, 2, 12, 0.04f, 0.09f );

					chance *= Utils.Select( Difficulty, 1f, 1.1f, 1.2f );

					if ( Game.Random.Float( 0f, 1f ) < chance )
					{
						var eventType = fillerPool[Game.Random.Next( fillerPool.Count )];
						fillerPool.Remove( eventType );

						PlannedEvents[minute] = eventType;
						usedMinutes.Add( minute );
					}
				}
			}
		}

		//Log.Info( $"--------- Events ---------" );
		//foreach ( var pair in PlannedEvents.OrderBy( x => x.Key ) )
		//	Log.Info( $"{pair.Key}: {pair.Value}" );

		//CreateEvent( EventType.Cloudy );
	}

	void AddMinibossEvent( List<int> range, HashSet<int> usedMinutes )
	{
		int mb = PickRandomUnusedMinute( range, usedMinutes );
		if ( mb != -1 )
		{
			PlannedEvents[mb] = EventType.Miniboss;
			usedMinutes.Add( mb );
		}
	}

	void CheckForNewMinuteEvent()
	{
		//Log.Info( $"CheckForNewMinuteEvent: {CurrMinute}" );

		if(PlannedEvents.ContainsKey(CurrMinute))
		{
			CreateEvent( PlannedEvents[CurrMinute] );
		}

		// todo: after boss spawn, create random events
	}

	void HandleActiveEvents()
	{
		if ( IsGameOver )
			return;

		for(int i = ActiveEvents.Count - 1; i >= 0; i--)
		{
			var stageEvent = ActiveEvents.ElementAt( i ).Value;
			stageEvent.Update();
		}
	}

	void CreateEvent( EventType eventType )
	{
		Log.Info( $"CreateEvent: {eventType}" );

		int randSeed = Game.Random.Int( 0, 9999999 );
		StartEvent( eventType, randSeed );
	}

	[Rpc.Broadcast]
	void StartEvent( EventType eventType, int randSeed )
	{
		Log.Info( $"StartEvent: {eventType}" );

		StageEvent stageEvent = null;
		switch( eventType )
		{
			case EventType.Miniboss: stageEvent = new StageEventMiniboss(); break;
			case EventType.Snaker: stageEvent = new StageEventSnaker(); break;
			case EventType.EvilChest: stageEvent = new StageEventEvilChest(); break;
			case EventType.IcyGround: stageEvent = new StageEventIcyGround(); break;
			case EventType.Wind: stageEvent = new StageEventWind(); break;
			case EventType.Dark: stageEvent = new StageEventDark(); break;
			case EventType.Cloudy: stageEvent = new StageEventCloudy(); break;
			case EventType.Lava: stageEvent = new StageEventLava(); break;
			case EventType.Mushrooms: stageEvent = new StageEventMushrooms(); break;
			case EventType.HealingZones: stageEvent = new StageEventHealingZones(); break;
			case EventType.Werewolf: stageEvent = new StageEventWerewolf(); break;
		}

		if ( stageEvent == null )
			return;

		stageEvent.EventType = eventType;
		stageEvent.Start( randSeed );

		ActiveEvents.Add( eventType, stageEvent );
	}

	public void RemoveEvent( EventType eventType )
	{
		if ( !ActiveEvents.ContainsKey( eventType ) )
			return;

		ActiveEvents[eventType].Remove();

		ActiveEvents.Remove( eventType );
	}

	int PickRandomUnusedMinute( List<int> range, HashSet<int> used )
	{
		var available = range.Where( x => !used.Contains( x ) ).ToList();
		if ( available.Count == 0 ) 
			return -1;
		return available[Game.Random.Next( available.Count )];
	}

	void RestartEvents()
	{
		foreach ( var pair in ActiveEvents )
			pair.Value.Remove();

		ActiveEvents.Clear();
	}
}