Agenda/Difficulty.cs
using System;

namespace PlanetMeat;

public sealed class Difficulty : GameObjectSystem<Difficulty>
{
	private class Multiplier( string challenge, float step )
	{
		private float _mult = float.NaN;
		public float Mult => float.IsNaN( _mult ) ? BakeMultiplier() : _mult;

		public string ChallengeIdent { get; private set; } = challenge;

		public void UpdateMultiplier()
		{
			if ( !float.IsNaN( _mult ) )
				BakeMultiplier();
		}

		private float BakeMultiplier()
		{
			return _mult = MathF.Pow( step, Current.GetChallengeLevel( ChallengeIdent ) );
		}
	}

	private int HighestRank { get; set; } = 0;

	private Dictionary<string, int> ChallengeLevels { get; } = [];

	private readonly Dictionary<string, Multiplier> challengeMults = new()
	{
		["EnemyHp"] = new( "enemy_hp", 1.3f ),
		["EnemyDmg"] = new( "enemy_dmg", 1.3f ),
		["EnemySpeed"] = new( "enemy_speed", 1.15f ),
		["GroupCount"] = new( "group_count", 1.15f ),
		["GroupDelay"] = new( "group_delay", 0.85f ),
		["GroupSize"] = new( "group_size", 1.2f ),
		["GroupSpread"] = new( "group_spread", 1.3f ),
	};

	private readonly HashSet<string> challengeFlags = [];

	public Difficulty( Scene scene ) : base( scene )
	{
		if ( !Scene.IsEditor )
		{
			foreach ( var c in ResourceLibrary.GetAll<Challenge>() )
			{
				var rank = c.Rank;
				if ( rank > HighestRank )
					HighestRank = rank;
			}
		}
	}

	public void ResetChallenges()
	{
		challengeFlags.Clear();
		ChallengeLevels.Clear();
		foreach ( var m in challengeMults.Values )
			m.UpdateMultiplier();
	}

	public static Challenge GetChallengeResource( string ident ) => ResourceLibrary.Get<Challenge>( "challenges/" + ident + ".chalng" );
	public int GetChallengeLevel( string ident ) => ChallengeLevels.GetValueOrDefault( ident, 0 );
	public static int GetChallengeMax( string ident ) => GetChallengeResource( ident ).MaxLevel;
	public bool HasChallenge( string ident ) => ChallengeLevels.ContainsKey( ident );
	public float GetMult( string id ) => challengeMults[id].Mult;

	public void AddChallenge( Challenge challenge )
	{
		var ident = challenge.Ident;
		ChallengeLevels[ident] = GetChallengeLevel( ident ) + 1;

		foreach ( var f in challenge.AddedFlags )
			AddFlag( f );

		foreach ( var m in challengeMults.Values.Where( ( v ) => v.ChallengeIdent == ident ) )
			m.UpdateMultiplier();
	}

	private void AddFlag( string flag )
	{
		if ( challengeFlags.Add( flag ) )
		{
			/* flag-specific behavior
			switch ( flag )
			{
				default:
					break;
			}
			*/
		}
	}

	private bool IsChallengeAvailable( Challenge c )
	{
		if ( !c.RequiredFlags.IsSubsetOf( challengeFlags ) )
			return false;

		var ident = c.Ident;
		var level = GetChallengeLevel( ident );
		var maxLevel = c.MaxLevel;
		return maxLevel < 0 || level < maxLevel;
	}

	private int GetEffectiveRank( Challenge c )
	{
		var level = GetChallengeLevel( c.Ident );
		var maxLevel = c.MaxLevel;
		return (maxLevel >= 0 && level < maxLevel) ? (c.Rank + level) : c.Rank;
	}

	private bool IsChallengeOfferedAtRank( Challenge c, int rank ) => GetEffectiveRank( c ) <= rank;

	private List<Challenge> GetChoicesAtRank( int rank, int minChoices )
	{
		var available = ResourceLibrary.GetAll<Challenge>().Where( IsChallengeAvailable );
		var offers = available.Where( ( c ) => IsChallengeOfferedAtRank( c, rank ) ).ToList();

		while ( offers.Count < minChoices && rank <= HighestRank )
		{
			offers.AddRange( available.Except( offers ).Where( ( c ) => IsChallengeOfferedAtRank( c, rank ) ) );
			rank++;
		}

		return offers;
	}

	public List<Challenge> CreateOffers( int rank, int numChoices = 3 )
	{
		var choices = GetChoicesAtRank( rank, numChoices );
		choices.Shuffle( Game.Random );

		/* DBG test specific challenges
		choices.Insert( 0, GetChallengeResource( "add_morsel" ) );
		choices.Insert( 0, GetChallengeResource( "add_feast" ) );
		choices.Insert( 0, GetChallengeResource( "hydra" ) );
		//*/

		if ( choices.Count > 0 )
			return choices[..numChoices];
		return [];
	}
}