manager/Manager.Events.cs

Event management portion of the game Manager class. It plans timed stage events, creates and starts events (including RPC broadcast StartEvent), tracks active events, updates them each tick, and removes or restarts 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();
	}
}