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.
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();
}
}