ui/PerkUnlockHelper.cs

Helper static class for perk unlocking logic. Computes which perk categories are currently available by reading PerkAttribute on types, filters eligible locked perks for offering to a player, and provides a weight lookup for perk rarity.

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

public static class PerkUnlockHelper
{
	public static readonly HashSet<PerkCategory> AlwaysAvailableCategories = new()
	{
		PerkCategory.AfterFirstPerk,
		PerkCategory.FireAndFreeze,
		PerkCategory.OneHpLeftAndDodge,
	};

	/// <summary>
	/// Returns the set of PerkCategories that are included by all currently available
	/// (unlocked or not locked) perks. Used to gate which locked perks can be offered.
	/// Reads from PerkAttribute directly — no perk instantiation needed.
	/// </summary>
	public static HashSet<PerkCategory> GetAvailableCategories( List<TypeDescription> allPerkTypes )
	{
		var result = new HashSet<PerkCategory>();
		foreach ( var t in allPerkTypes )
		{
			var a = t.GetAttribute<PerkAttribute>();
			bool isAvailable = !a.Locked || ProgressManager.IsLockedPerkUnlocked( t );
			if ( !isAvailable ) continue;
			if ( a.IncludedCategories == null ) continue;
			foreach ( var cat in a.IncludedCategories )
				result.Add( cat );
		}
		return result;
	}

	/// <summary>
	/// Returns locked perks eligible to be offered as an unlock choice.
	/// A perk is eligible if it is locked, not yet unlocked, and either IncludedAtStart
	/// or belongs to a category that is available or always-available.
	/// </summary>
	public static List<TypeDescription> GetEligibleLockedPerks(
		List<TypeDescription> allPerkTypes,
		HashSet<PerkCategory> availableCategories,
		Player player )
	{
		return allPerkTypes.Where( t =>
		{
			var a = t.GetAttribute<PerkAttribute>();
			if ( !a.Locked ) return false;
			if ( ProgressManager.IsLockedPerkUnlocked( t ) ) return false;
			if ( a.MinUnlocksReq > 0 && ProgressManager.UnlockedLockedPerksCount < a.MinUnlocksReq ) return false;
			if ( a.IncludedAtStart ) return true;
			if ( !player.IsValid() ) return false;
			foreach ( var kvp in player.PerkCategories )
			{
				if ( !AlwaysAvailableCategories.Contains( kvp.Key ) ) continue;
				if ( kvp.Value.Contains( t.TargetType ) ) return true;
			}
			foreach ( var kvp in player.PerkCategories )
			{
				if ( !availableCategories.Contains( kvp.Key ) ) continue;
				if ( kvp.Value.Contains( t.TargetType ) ) return true;
			}
			return false;
		} ).ToList();
	}

	public static float WeightForRarity( Rarity r ) => r switch
	{
		Rarity.Common    => 1.7f,
		Rarity.Uncommon  => 1.5f,
		Rarity.Rare      => 1.25f,
		Rarity.Epic      => 1.0f,
		Rarity.Mythic    => 0.9f,
		Rarity.Legendary => 0.75f,
		Rarity.Unique    => 0.6f,
		_                => 1.0f,
	};
}