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,
};
}