PerkManager.cs

PerkManager and PerkAttribute definitions for the game's perk system. PerkAttribute stores metadata for perks. PerkManager contains logic to filter, weight and pick random perks for players, determine allowed perks/rewards, create Perk instances, map type identities, and provide rarity colors/names and utility functions.

Reflection
using System;
using System.Diagnostics.SymbolStore;
using Sandbox;

public class PerkAttribute : Attribute
{
	public Rarity Rarity;
	public bool IncludedAtStart;
	public MultiplayerMode MultiplayerMode;
	public bool AlwaysOfferDebug;
	public bool Disabled;
	public bool Curse;
	public bool Locked;
	public int MinDifficulty;
	public int MaxDifficulty;
	public bool OnlyUnpausedChoosing;
	public ControlMode ControlMode;
	public PerkCategory[] IncludedCategories { get; set; }
	public int MinUnlocksReq;

	public PerkAttribute( Rarity rarity, bool includedAtStart = true, MultiplayerMode multiplayerMode = MultiplayerMode.Always, bool alwaysOfferDebug = false, bool disabled = false, bool curse = false,
		int minDifficulty = Manager.MinDifficulty, int maxDifficulty = Manager.MaxDifficulty, bool onlyUnpausedChoosing = false, ControlMode controlMode = ControlMode.All, bool locked = false, int minUnlocksReq = 0 )
	{
		Rarity = rarity;
		IncludedAtStart = includedAtStart;
		MultiplayerMode = multiplayerMode;
		AlwaysOfferDebug = alwaysOfferDebug;
		Disabled = disabled;
		Curse = curse;
		Locked = locked;
		MinDifficulty = minDifficulty;
		MaxDifficulty = maxDifficulty;
		OnlyUnpausedChoosing = onlyUnpausedChoosing;
		ControlMode = controlMode;
		MinUnlocksReq = minUnlocksReq;
	}
}

public enum CurseSelection { NoCurses, AllowCurses, OnlyCurses }
public enum ControlMode { All, MouseAndKeyboard, Controller }

public class PerkManager
{
	public static List<TypeDescription> GetRandomPerks( Player player, int numPerks, Rarity rarity, CurseSelection curseSelection, bool isChoice, List<TypeDescription> notAllowed = null, bool isReward = false, bool includeLocked = false )
	{
		var manager = Manager.Instance;

		List<(TypeDescription Type, float Weight)> valid = new();

		foreach ( var typeIdent in player.ValidPerks )
		{
			TypeDescription type = IdentityToType( typeIdent );

			var attrib = type.GetAttribute<PerkAttribute>();
			if ( attrib == null )
				continue;

			if ( attrib.Disabled )
				continue;

			var alwaysOfferDebug = Game.IsEditor && attrib.AlwaysOfferDebug;

			//if ( attrib.Locked && !includeLocked && !alwaysOfferDebug && !Manager.HideProgressionSystem && !ProgressManager.IsLockedPerkUnlocked( type ) )
			//	continue;

			if ( attrib.MultiplayerMode == MultiplayerMode.OnlyMultiplayer && !manager.IsMultiplayer && !(alwaysOfferDebug && isChoice) )
				continue;

			if ( attrib.MultiplayerMode == MultiplayerMode.OnlySingleplayer && manager.IsMultiplayer && !(alwaysOfferDebug && isChoice) )
				continue;

			if ( manager.Difficulty < attrib.MinDifficulty || manager.Difficulty > attrib.MaxDifficulty )
				continue;

			if ( attrib.OnlyUnpausedChoosing && !manager.IsUnpausedChoosing && !alwaysOfferDebug )
				continue;

			if ( attrib.ControlMode == ControlMode.Controller && !Input.UsingController )// && !alwaysOfferDebug )
				continue;

			if ( attrib.ControlMode == ControlMode.MouseAndKeyboard && Input.UsingController )// && !alwaysOfferDebug )
				continue;

			if ( rarity != Rarity.None && rarity != attrib.Rarity )
				continue;

			if ( ((curseSelection == CurseSelection.NoCurses && attrib.Curse) || (curseSelection == CurseSelection.OnlyCurses && !attrib.Curse)) && !alwaysOfferDebug )
				continue;

			int existingLevel = player.GetPerkLevel( type );
			if ( existingLevel >= GetMaxLevelForRarity( attrib.Rarity ) )
				continue;

			if ( notAllowed != null && notAllowed.Contains( type ) )
				continue;

			if ( !IsPerkAllowed( type, player, isChoice ) )// && !alwaysOfferDebug )
				continue;

			if ( isReward && !IsPerkAllowedAsReward( type, player ) )
				continue;

			float weight = GetWeightForRarity( attrib.Rarity, player, attrib.Curse );

			if ( existingLevel > 0 && player.Stats[PlayerStat.ExistingPerkChance] > 0f )
				weight *= (1f + existingLevel * player.Stats[PlayerStat.ExistingPerkChance]);

			if ( alwaysOfferDebug )
				weight = 99999999999f;

			valid.Add( (type, weight) );
		}

		if ( isChoice )
			PerkSeeOtherChoices.AdjustForSeenPerks( player, valid, numPerks );

		List<TypeDescription> output = new List<TypeDescription>();

		int validCount = valid.Count;
		while ( output.Count < Math.Min( numPerks, validCount ) )
		{
			float totalWeight = valid.Sum( x => x.Weight );
			var rand = Game.Random.Float( 0f, totalWeight );

			for ( int i = valid.Count - 1; i >= 0; i-- )
			{
				var (type, weight) = valid[i];
				rand -= weight;

				if ( rand < 0f )
				{
					output.Add( type );
					valid.Remove( (type, weight) );
					break;
				}
			}
		}

		return output;
	}

	//public static List<TypeDescription> GetRandomPerks( Player player, int numPerks, Rarity rarity, CurseSelection curseSelection, bool isChoice, List<TypeDescription> notAllowed = null )
	//{
	//	List<TypeDescription> output = new();

	//	while ( output.Count < numPerks )
	//	{
	//		var type = GetRandomPerk( player, rarity, curseSelection, isChoice, output );

	//		// todo: if null, find another valid perk of a different rarity
	//		if( type != null )
	//		{
	//			output.Add( GetRandomPerk( player, rarity, curseSelection, isChoice, output ) );
	//		}
	//	}

	//	return output;
	//}

	//public static TypeDescription GetRandomPerk( Player player, Rarity rarity, CurseSelection curseSelection, bool isChoice, List<TypeDescription> notAllowed = null )
	//{
	//	if ( rarity == Rarity.None )
	//		rarity = GetRarityBasedOnChance( player, isCurse: curseSelection == CurseSelection.OnlyCurses );

	//	List<(TypeDescription Type, float Weight)> valid = new();

	//	foreach ( var typeIdent in player.ValidPerks )
	//	{
	//		TypeDescription type = IdentityToType( typeIdent );

	//		var attrib = type.GetAttribute<PerkAttribute>();
	//		if ( attrib == null )
	//			continue;

	//		if ( attrib.Disabled )
	//			continue;

	//		if ( attrib.OnlyForMultiplayer && !Manager.Instance.IsMultiplayer && !(attrib.AlwaysOfferDebug && isChoice) )
	//			continue;

	//		if ( Manager.Instance.Difficulty < attrib.MinDifficulty || Manager.Instance.Difficulty > attrib.MaxDifficulty )
	//			continue;

	//		if ( rarity != attrib.Rarity )
	//			continue;

	//		if ( ((curseSelection == CurseSelection.NoCurses && attrib.Curse) || (curseSelection == CurseSelection.OnlyCurses && !attrib.Curse)) && !attrib.AlwaysOfferDebug )
	//			continue;

	//		int existingLevel = player.GetPerkLevel( type );
	//		if ( existingLevel >= GetMaxLevelForRarity( attrib.Rarity ) )
	//			continue;

	//		if ( notAllowed != null && notAllowed.Contains( type ) )
	//			continue;

	//		if ( !IsPerkAllowed( type, player, isChoice ) )
	//			continue;

	//		float weight = 1f;

	//		if ( existingLevel > 0 && player.Stats[PlayerStat.ExistingPerkChance] > 0f )
	//			weight *= (1f + existingLevel * player.Stats[PlayerStat.ExistingPerkChance]);

	//		if ( attrib.AlwaysOfferDebug )
	//			weight = 99999999999f;

	//		valid.Add( (type, weight) );
	//	}

	//	float totalWeight = valid.Sum( x => x.Weight );
	//	var rand = Game.Random.Float( 0f, totalWeight );

	//	for ( int i = valid.Count - 1; i >= 0; i-- )
	//	{
	//		var (type, weight) = valid[i];
	//		rand -= weight;

	//		if ( rand < 0f )
	//		{
	//			return type;
	//		}
	//	}

	//	return null;
	//}

	public static bool IsPerkAllowed( TypeDescription type, Player player, bool isChoice )
	{
		if ( type == TypeLibrary.GetType( typeof( CursePiercing ) ) && player.Stats[PlayerStat.BulletNumPiercing] == 0f && player.Stats[PlayerStat.BulletExtraPierceChance] == 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseCritChance ) ) && !(player.Stats[PlayerStat.CritChance] > 0f ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseCritMultiplier ) ) && !(player.Stats[PlayerStat.CritMultiplier] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( CursePiercing ) ) && !( player.Stats[PlayerStat.BulletNumPiercing] > 0f || player.Stats[PlayerStat.BulletExtraPierceChance] > 0f ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseMaxHp ) ) && !(player.Stats[PlayerStat.MaxHp] > 1f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBanishOtherChoices ) ) && !isChoice ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashChangeDirection ) ) && (player.Stats[PlayerStat.Blink] > 0f || player.Stats[PlayerStat.JumpNotDash] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashLength ) ) && player.Stats[PlayerStat.BlinkToCursor] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashSlash ) ) && (player.Stats[PlayerStat.Blink] > 0f || player.Stats[PlayerStat.JumpNotDash] > 0f) && player.GetPerkLevel( TypeLibrary.GetType( typeof( PerkDashSlash ) ) ) == 0 ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkPacifistDmgUnderKills ) ) && (int)player.Stats[PlayerStat.NumEnemiesKilled] >= 100 ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMaxAmmoDownside ) ) && player.Perks.Count <= 1 && player.HasPerk( TypeLibrary.GetType( typeof( PerkMaxAmmoDownside ) ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkCleanse ) ) && player.Perks.Count <= (player.HasPerk( TypeLibrary.GetType( typeof( PerkCleanse ) ) ) ? 2 : 1 ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNoInvulnDash ) ) && player.HasPerk( TypeLibrary.GetType( typeof( CurseNoDashInvuln ) ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseNoDashInvuln ) ) && player.HasPerk( TypeLibrary.GetType( typeof( PerkNoInvulnDash ) ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseNumDashes ) ) && player.Stats[PlayerStat.NumDashes] <= 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseMaxAmmo ) ) && player.Stats[PlayerStat.MaxAmmoCount] <= 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessAmmo ) ) && player.Stats[PlayerStat.MaxAmmoCount] <= 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessAmmoArcBullets ) ) && player.Stats[PlayerStat.MaxAmmoCount] <= 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashLessAmmo ) ) && player.Stats[PlayerStat.MaxAmmoCount] <= 0f ) return false;
		if ( player.Stats[PlayerStat.NumPerkChoices] <= 0f && (type == TypeLibrary.GetType( typeof( PerkNumChoices ) ) || type == TypeLibrary.GetType( typeof( CurseNumChoices ) ) || type == TypeLibrary.GetType( typeof( PerkSkipChoices ) ) || type == TypeLibrary.GetType( typeof( PerkRandomChoice ) )) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkCommunism ) ) && Manager.Instance.IsCommunismActive ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHandoutsRemove ) ) && player.Perks.Count <= 1 && player.HasPerk( TypeLibrary.GetType( typeof( PerkHandoutsRemove ) ) ) ) return false;
		if ( player.PerkCategories[PerkCategory.Dodge].Contains( type.TargetType ) && !((player.Stats[PlayerStat.DodgeChance] > 0f) || player.Stats[PlayerStat.DodgeGuaranteedNum] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRandomFavorite ) ) && player.FavoritePerks.Count <= 0 ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseMoveRelativeToAimDir ) ) && player.Stats[PlayerStat.FpsMode] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkFpsMode ) ) && player.Stats[PlayerStat.MovementRelativeToAimDir] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkPerspective ) ) && player.Stats[PlayerStat.PerspectiveCamera] > 0f ) return false;
		// todo: if have reduced crit chance curse, maybe don't allow reduce crit multiplier curse and vice versa? or at least make it less likely?
		if ( type == TypeLibrary.GetType( typeof( PerkBossSpawnEarly ) ) && Manager.Instance.HasSpawnedBoss ) return false;
		if ( type == TypeLibrary.GetType( typeof( CurseProjectileBounceFence ) ) && Manager.Instance.EnemyProjectileBounceFenceLevel > 0 ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMaxHealth ) ) && player.Stats[PlayerStat.MaxHpCap] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( CursePreventPausing ) ) && player.Stats[PlayerStat.UnpausedChoosing] > 0f ) return false;

		//if ( player.Stats[PlayerStat.PunchBullets] > 0f && DoesPerkNotWorkWithPunch( type ) ) return false;

		return true;
	}

	public static bool IsPerkAllowedAsReward( TypeDescription type, Player player )
	{
		if ( type == TypeLibrary.GetType( typeof( PerkHandouts ) ) ) return false;
		//if ( type == TypeLibrary.GetType( typeof( PerkHandoutsCommon ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHandoutsRemove ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMaxAmmoDownside ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkCleanse ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkEvilChest ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkIncreaseEarliestPerk ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkTimerDigitDamage ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNumChoicesCantMove ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRerolls ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNumChoices ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessAmmoArcBullets ) ) && player.Stats[PlayerStat.MaxAmmoCount] <= 0f ) return false;

		// for the remaining perks, if we've already chosen it, then we can allow it as reward
		if ( player.GetPerkLevel( type ) > 0 )
			return true;

		if ( type == TypeLibrary.GetType( typeof( PerkPunch ) ) && !(player.Stats[PlayerStat.PunchBullets] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletKickback ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletNegativeKickback ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkArtillery ) ) && !(player.Stats[PlayerStat.ExplosionDamageMultiplier] > 0f || player.Stats[PlayerStat.ExplosionSizeMultiplier] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLandmine ) ) && !(player.Stats[PlayerStat.ExplosionDamageMultiplier] > 0f || player.Stats[PlayerStat.ExplosionSizeMultiplier] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkAlternateAimDir ) ) && !(player.Stats[PlayerStat.BulletHomingRadius] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkAutoReroll ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBerserk ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBossSpawnEarly ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRandomExisting ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBoomerangHurtSelf ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletHurtSelfBounce ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletHomingHurt ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletHomingGround ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletHomingCursed ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkChooseTimeLimit ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkClickRemove ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkCommunism ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashCapMaxHp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashRandomlyWhenHit ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashReload ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashToBlink ) ) && player.Stats[PlayerStat.DashSlashBulletDamagePercent] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashToJump ) ) && player.Stats[PlayerStat.DashSlashBulletDamagePercent] > 0f ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDashSlash ) ) && (player.Stats[PlayerStat.JumpNotDash] > 0f || player.Stats[PlayerStat.Blink] > 0f) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDoubleDashes ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDoubleMaxHp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDrainHealOtherPlayers ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkDrainUntil1Hp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkFpsMode ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkFriendlyDmgCoin ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkFullHealthAoe ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHealOnKill ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHealthPackMaxHp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHealthPackXp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkHurtSelfOnKill ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessAmmo ) ) && player.GetPerkLevel( TypeLibrary.GetType( typeof( PerkLessAmmoLongerLifetime ) ) ) == 0 ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessAmmoLongerLifetime ) ) && player.GetPerkLevel( TypeLibrary.GetType( typeof( PerkLessAmmo ) ) ) == 0 ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLessChoiceMoreDamage ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLoseHpBeforeArmorAt1Hp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkLoseWhenGainXp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoreRerolls ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoreRerollsLoseHp ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoveDrainHealth ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoveSlowerWhileShooting ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoveSpeedAlignment ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMoveToSpawnFire ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNoCommons ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNoInvulnDash ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNoReqs ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNumChoicesTimed ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkPacifistDmgUnderKills ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRadiation ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRandomUnique ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkMiniPlayer ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRestartChoices ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkSelfDmgDash ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkShieldMinDmg ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkShootOnlyWhenClick ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkShotgun ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkShuffleSideToSide ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkUpsideDown ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkXpGainMultiplier ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkZoomContinuously ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkZoomWobble ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkPierce ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkNumProjectiles ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkAttackSpeedLessDamage ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkRecoil) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkZoomIn ) ) ) return false;
		if ( type == TypeLibrary.GetType( typeof( PerkBulletDmgFacing ) ) ) return false;

		return true;
	}

	public static Perk CreatePerk( TypeDescription type )
	{
		var Perk = type.Create<Perk>();

		var attrib = type.GetAttribute<PerkAttribute>();
		if ( attrib != null )
			Perk.MaxLevel = GetMaxLevelForRarity( attrib.Rarity );

		return Perk;
	}

	public static int TypeToIdentity( TypeDescription type )
	{
		return type.Identity;
	}

	public static TypeDescription IdentityToType( int typeIdentity )
	{
		return TypeLibrary.GetTypeByIdent( typeIdentity );
	}

	public static Color GetCardRarityColor( Rarity rarity, bool curse = false, float alpha = 1f )
	{
		Color color;
		switch ( rarity )
		{
			case Rarity.Common: default: color = new Color( 0.75f, 0.75f, 0.75f ); break;
			case Rarity.Uncommon: color = new Color( 0.6f, 0.6f, 0.9f ); break;
			case Rarity.Rare: color = new Color( 1f, 0.3f, 0.6f ); break;
			case Rarity.Epic: color = new Color( 1f, 0.5f, 0f ); break;
			case Rarity.Mythic: color = new Color( 0.9f, 0f, 1f ); break;
			case Rarity.Legendary: color = new Color( 0f, 0.8f, 0f ); break;
			case Rarity.Unique: color = new Color( 1f, 1f, 0.3f ); break;
		}

		if ( curse )
			color = Color.Lerp( new Color( 0.9f, 0f, 1f ), Color.Black, 0.5f );

		return color.WithAlpha( alpha );
	}

	public static Color GetFontRarityColor( Rarity rarity, bool curse = false, float alpha = 1f )
	{
		Color color;
		switch ( rarity )
		{
			case Rarity.Common: default: color = new Color( 0.75f, 0.75f, 0.75f ); break;
			case Rarity.Uncommon: color = new Color( 0.6f, 0.6f, 0.9f ); break;
			case Rarity.Rare: color = new Color( 1f, 0.3f, 0.6f ); break;
			case Rarity.Epic: color = new Color( 1f, 0.5f, 0f ); break;
			case Rarity.Mythic: color = new Color( 0.9f, 0f, 1f ); break;
			case Rarity.Legendary: color = new Color( 0f, 0.8f, 0f ); break;
			case Rarity.Unique: color = new Color( 1f, 1f, 0.3f ); break;
		}

		if ( curse )
			color = Color.Lerp( new Color( 0.9f, 0f, 1f ), Color.Black, 0.25f );

		return color.WithAlpha( alpha );
	}

	public static string GetCardRarityName( Rarity rarity, bool curse = false )
	{
		string name;
		switch ( rarity )
		{
			case Rarity.Common: default: name = "Common"; break;
			case Rarity.Uncommon: name = "Uncommon"; break;
			case Rarity.Rare: name = "Rare"; break;
			case Rarity.Epic: name = "Epic"; break;
			case Rarity.Mythic: name = "Mythic"; break;
			case Rarity.Legendary: name = "Legendary"; break;
			case Rarity.Unique: name = "Unique"; break;
		}

		if ( curse )
			name = "Curse";

		return name;
	}

	public static float GetWeightForRarity( Rarity rarity, Player player, bool isCurse )
	{
		float weight;

		if ( !isCurse )
		{
			switch ( rarity )
			{
				////case Rarity.Common: default: weight = Utils.Map( Manager.Instance.ElapsedTime, 0f, 30f, 600f, 200f ) * (1f + player.Stats[PlayerStat.RarityIncreaseCommon]); break;
				//case Rarity.Common: default: weight = Utils.Map( Manager.Instance.ElapsedTime, 0f, 30f, 450f, 320f ) * (1f + player.Stats[PlayerStat.RarityIncreaseCommon]); break;
				//case Rarity.Uncommon: weight = 160f * (1f + player.Stats[PlayerStat.RarityIncreaseUncommon]); break;
				//case Rarity.Rare: weight = 60f * (1f + player.Stats[PlayerStat.RarityIncreaseRare]); break;
				//case Rarity.Epic: weight = 35f * (1f + player.Stats[PlayerStat.RarityIncreaseEpic]); break;
				//case Rarity.Mythic: weight = 14.5f * (1f + player.Stats[PlayerStat.RarityIncreaseMythic]); break;
				//case Rarity.Legendary: weight = 8.0f * (1f + player.Stats[PlayerStat.RarityIncreaseLegendary]); break;
				//case Rarity.Unique: weight = 2.05f * (1f + player.Stats[PlayerStat.RarityIncreaseUnique]); break;
				//	//case Rarity.Curse: weight = 250f * (1f + player.Stats[PlayerStat.RarityIncreaseCurse]); break;

				case Rarity.Common: default: weight = 425f * (1f + player.Stats[PlayerStat.RarityIncreaseCommon]); break;
				case Rarity.Uncommon: weight = 183f * (1f + player.Stats[PlayerStat.RarityIncreaseUncommon]); break;
				case Rarity.Rare: weight = 75f * (1f + player.Stats[PlayerStat.RarityIncreaseRare]); break;
				case Rarity.Epic: weight = 46f * (1f + player.Stats[PlayerStat.RarityIncreaseEpic]); break;
				case Rarity.Mythic: weight = 25f * (1f + player.Stats[PlayerStat.RarityIncreaseMythic]); break;
				case Rarity.Legendary: weight = 15f * (1f + player.Stats[PlayerStat.RarityIncreaseLegendary]); break;
				case Rarity.Unique: weight = 3f * (1f + player.Stats[PlayerStat.RarityIncreaseUnique]); break;
			}
		}
		else
		{
			weight = 200f;
		}

		return weight;
	}

	public static int GetMaxLevelForRarity( Rarity rarity )
	{
		switch ( rarity )
		{
			case Rarity.Common: default: return 7;
			case Rarity.Uncommon: return 6;
			case Rarity.Rare: return 5;
			case Rarity.Epic: return 4;
			case Rarity.Mythic: return 3;
			case Rarity.Legendary: return 2;
			case Rarity.Unique: return 1;
		}
	}

	public static int GetAvailableCurseCount( Player player )
	{
		var manager = Manager.Instance;
		int count = 0;
		// List<string> availableCurseNames = new();

		foreach ( var typeIdent in player.ValidPerks )
		{
			TypeDescription type = IdentityToType( typeIdent );
			var attrib = type.GetAttribute<PerkAttribute>();

			if ( attrib == null )
				continue;
			if ( attrib.Disabled )
				continue;
			if ( !attrib.Curse )
				continue;
			if ( attrib.MultiplayerMode == MultiplayerMode.OnlyMultiplayer && !manager.IsMultiplayer )
				continue;
			if ( attrib.MultiplayerMode == MultiplayerMode.OnlySingleplayer && manager.IsMultiplayer )
				continue;
			if ( manager.Difficulty < attrib.MinDifficulty || manager.Difficulty > attrib.MaxDifficulty )
				continue;
			if ( !IsPerkAllowed( type, player, isChoice: true ) )
				continue;

			int existingLevel = player.GetPerkLevel( type );
			if ( existingLevel < GetMaxLevelForRarity( attrib.Rarity ) )
			{
				count++;
				// availableCurseNames.Add( type.Name );
			}
		}
		// Log.Info( $"Available curses ({count}): {string.Join( ", ", availableCurseNames )}" );
		return count;
	}

	public static Rarity GetRarityBasedOnChance( Player player, bool isCurse )
	{
		Dictionary<Rarity, float> weightedValues = new();
		for ( int i = 1; i <= 7; i++ )
		{
			Rarity rarity = (Rarity)i;
			weightedValues.Add( rarity, GetWeightForRarity( rarity, player, isCurse ) );
		}

		var total = 0f;
		foreach ( var pair in weightedValues )
			total += pair.Value;

		var rand = Game.Random.Float( 0f, total );

		var currentTotal = 0f;
		foreach ( var pair in weightedValues )
		{
			var rarity = pair.Key;
			currentTotal += pair.Value;
			if ( rand < currentTotal )
			{
				return rarity;
			}
		}

		return Rarity.None;
	}

	/// <summary>
	/// Some perks don't work with punches, but the punching player can still spawn normal bullets other ways...
	/// </summary>
	/// <param name="type"></param>
	/// <returns></returns>
	public static bool DoesPerkNotWorkWithPunch( TypeDescription type )
	{
		if (
			type == TypeLibrary.GetType( typeof( PerkBulletHomingRadius ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletHomingHurt ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletHomingGround ) ) ||
			type == TypeLibrary.GetType( typeof( PerkOnly1HpBulletHoming ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletHomingCursed ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletArcStill ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletAimAtCursor ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletOverflow ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletReturn ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBounceTarget ) ) ||
			type == TypeLibrary.GetType( typeof( PerkPierce ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBounce ) ) ||
			type == TypeLibrary.GetType( typeof( PerkPierceChance ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBounceChance ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletHealTeammate ) ) || // todo: enable punch to heal teammates (should be green colored punch)
			type == TypeLibrary.GetType( typeof( PerkBulletMoveRandomly ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBounceDamage ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletSplash ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBounceResetLifetime) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletDistanceDamage ) ) ||
			type == TypeLibrary.GetType( typeof( PerkBulletGrow) )

			// ...
		)
			return true;

		return false;
	}
}