Base Perk class used by the game. It stores registration metadata (name, image, descriptions, tooltip, layout hints) for perk subclasses, provides lookup and rich-text helper methods, manages perk state (level, timing, display/animation properties), and declares many virtual callback methods that concrete perks override.
#nullable enable
using System;
using System.Numerics;
using System.Reflection;
using System.Text.RegularExpressions;
using Sandbox;
public enum Rarity { None, Common, Uncommon, Rare, Epic, Mythic, Legendary, Unique }
public enum MultiplayerMode { Always, OnlySingleplayer, OnlyMultiplayer }
public enum PerkCategory { AfterFirstPerk, Fire, Freeze, FireAndFreeze, Poison, Fear, SelfDmg, Dodge, Explosion, BulletBounce, BulletPierce, Armor, Shield, Radiation, Landmine, Aoe, NumProjectiles,
Bomb, Blink, Boomerang, Punch, OneHpLeft, OneHpLeftAndDodge, Jump, Artillery, OrbiterBlade, ArcBullets, }
public abstract class Perk : IStatModifier, IPlayerCallbacks
{
private static readonly Dictionary<Type, string> _names = new();
private static readonly Dictionary<string, string> _namesByFullName = new();
private static readonly Dictionary<Type, string> _imagePaths = new();
private static readonly Dictionary<string, string> _imagePathsByFullName = new();
private static readonly Dictionary<Type, Func<int, string>> _descriptions = new();
private static readonly Dictionary<string, Func<int, string>> _descriptionsByFullName = new();
private static readonly Dictionary<Type, Func<int, string>> _upgradeDescriptions = new();
private static readonly Dictionary<string, Func<int, string>> _upgradeDescriptionsByFullName = new();
private static readonly Dictionary<Type, string> _tooltipInfos = new(); // Optional Tooltip
private static readonly Dictionary<string, string> _tooltipInfosByFullName = new();
private static readonly Dictionary<Type, float> _descriptionLineHeights = new(); // Optional line height
private static readonly Dictionary<string, float> _descriptionLineHeightsByFullName = new();
private static readonly Dictionary<Type, float> _descriptionFontSizes = new(); // Optional font size
private static readonly Dictionary<string, float> _descriptionFontSizesByFullName = new();
private static readonly Dictionary<Type, float> _descriptionImageSizes = new(); // Optional image size
private static readonly Dictionary<string, float> _descriptionImageSizesByFullName = new();
private static readonly HashSet<Type> _registeredTypes = new();
protected static void Register<T>(
string name,
string imagePath,
Func<int, string> description,
Func<int, string>? upgradeDescription = null, // Optional
string? tooltipInfo = null, // Optional
float? descriptionLineHeight = null, // Optional
float? descriptionFontSize = null, // Optional
float? descriptionImageSize = null // Optional
) where T : Perk
{
var fullName = typeof( T ).FullName ?? typeof( T ).Name;
_names[typeof( T )] = name;
_namesByFullName[fullName] = name;
_imagePaths[typeof( T )] = imagePath;
_imagePathsByFullName[fullName] = imagePath;
_descriptions[typeof( T )] = description;
_descriptionsByFullName[fullName] = description;
if ( upgradeDescription != null ) // Only store it if provided
{
_upgradeDescriptions[typeof( T )] = upgradeDescription;
_upgradeDescriptionsByFullName[fullName] = upgradeDescription;
}
if ( tooltipInfo != null ) // Only store it if provided
{
_tooltipInfos[typeof( T )] = tooltipInfo;
_tooltipInfosByFullName[fullName] = tooltipInfo;
}
if ( descriptionLineHeight != null ) // Only store it if provided
{
_descriptionLineHeights[typeof( T )] = descriptionLineHeight.Value;
_descriptionLineHeightsByFullName[fullName] = descriptionLineHeight.Value;
}
if ( descriptionFontSize != null ) // Only store it if provided
{
_descriptionFontSizes[typeof( T )] = descriptionFontSize.Value;
_descriptionFontSizesByFullName[fullName] = descriptionFontSize.Value;
}
if ( descriptionImageSize != null ) // Only store it if provided
{
_descriptionImageSizes[typeof( T )] = descriptionImageSize.Value;
_descriptionImageSizesByFullName[fullName] = descriptionImageSize.Value;
}
}
private static string GetLookupKey( Type type )
{
return type?.FullName ?? type?.Name ?? string.Empty;
}
private static void EnsureRegistered( Type type )
{
if ( type == null || _registeredTypes.Contains( type ) )
return;
var typeDescription = TypeLibrary.GetType( type );
if ( typeDescription == null )
return;
// Creating an instance ensures the perk's static Register<>() data exists on clients
// that only know the perk by networked identity and have never owned the perk.
typeDescription.Create<Perk>();
_registeredTypes.Add( type );
}
public static string? GetName( Type type )
{
EnsureRegistered( type );
return _names.TryGetValue( type, out var name )
? name
: (_namesByFullName.TryGetValue( GetLookupKey( type ), out name ) ? name : null);
}
public static string? GetImagePath( Type type )
{
EnsureRegistered( type );
return _imagePaths.TryGetValue( type, out var imagePath )
? imagePath
: (_imagePathsByFullName.TryGetValue( GetLookupKey( type ), out imagePath ) ? imagePath : null);
}
public static string NormalizeLooseName( string? input )
{
if ( string.IsNullOrWhiteSpace( input ) )
return "";
input = input.ToLowerInvariant();
return Regex.Replace( input, @"[^a-z0-9]", "" );
}
public static bool AreLooseNamesEqual( string? str1, string? str2 )
{
if ( string.IsNullOrWhiteSpace( str1 ) || string.IsNullOrWhiteSpace( str2 ) )
return false;
return NormalizeLooseName( str1 ).Equals( NormalizeLooseName( str2 ), StringComparison.Ordinal );
}
public static TypeDescription? FindTypeByLooseDisplayName( string? message, bool includeDisabled = false )
{
var normalizedMessage = NormalizeLooseName( message );
if ( string.IsNullOrEmpty( normalizedMessage ) )
return null;
foreach ( var type in TypeLibrary.GetTypes<Perk>() )
{
var attrib = type.GetAttribute<PerkAttribute>();
if ( attrib == null )
continue;
if ( !includeDisabled && attrib.Disabled )
continue;
var displayName = GetName( type.TargetType );
if ( string.IsNullOrWhiteSpace( displayName ) )
continue;
if ( AreLooseNamesEqual( normalizedMessage, displayName ) )
return type;
}
return null;
}
public static string GetRichTextToken( Type type, int? level = null )
{
if ( type == null )
return "";
return GetRichTextToken( type.Name, level );
}
public static string GetRichTextToken( TypeDescription type, int? level = null )
{
if ( type == null )
return "";
return GetRichTextToken( type.Name, level );
}
public static string GetRichTextToken( string perkTypeName, int? level = null )
{
if ( string.IsNullOrWhiteSpace( perkTypeName ) )
return "";
if ( level.HasValue && level.Value > 0 )
return $"[perk:{perkTypeName}:{level.Value}]";
return $"[perk:{perkTypeName}]";
}
public static string GetRichTextColorToken( string text, Color color )
{
if ( string.IsNullOrWhiteSpace( text ) )
return "";
return $"[color:{color.Hex}]{text}[/color]";
}
public static string GetRichTextRarityColorToken( string text, Rarity rarity, bool curse = false, float alpha = 1f )
{
var color = PerkManager.GetFontRarityColor( rarity, curse, alpha );
return GetRichTextColorToken( text, color );
}
public static string GetRichTextNameToken( Type type )
{
if ( type == null )
return "";
var name = GetName( type ) ?? type.Name;
var typeDescription = TypeLibrary.GetType( type );
var perkAttribute = typeDescription?.GetAttribute<PerkAttribute>();
if ( perkAttribute == null )
return name;
return GetRichTextRarityColorToken( name, perkAttribute.Rarity, perkAttribute.Curse );
}
public static string? GetDescription( Type type, int level )
{
EnsureRegistered( type );
if ( !_descriptions.TryGetValue( type, out var description ) )
_descriptionsByFullName.TryGetValue( GetLookupKey( type ), out description );
if ( description != null )
{
try
{
return description( level );
}
catch ( Exception ex )
{
Log.Warning( $"Failed to get description for perk '{type.Name}' (level {level}): {ex.Message}" );
return null;
}
}
return null;
}
public static string? GetUpgradeDescription( Type type, int level )
{
EnsureRegistered( type );
if ( !_upgradeDescriptions.TryGetValue( type, out var upgradeDescription ) )
_upgradeDescriptionsByFullName.TryGetValue( GetLookupKey( type ), out upgradeDescription );
if ( upgradeDescription != null )
{
try
{
return upgradeDescription( level );
}
catch ( Exception ex )
{
Log.Warning( $"Failed to get upgrade description for perk '{type.Name}' (level {level}): {ex.Message}" );
return null;
}
}
return null;
}
public static string? GetTooltipInfo( Type type )
{
EnsureRegistered( type );
return _tooltipInfos.TryGetValue( type, out var tooltip )
? tooltip
: (_tooltipInfosByFullName.TryGetValue( GetLookupKey( type ), out tooltip ) ? tooltip : null);
}
public static float? GetDescriptionLineHeight( Type type )
{
EnsureRegistered( type );
return _descriptionLineHeights.TryGetValue( type, out var lineHeight )
? lineHeight
: (_descriptionLineHeightsByFullName.TryGetValue( GetLookupKey( type ), out lineHeight ) ? lineHeight : null);
}
public static float? GetDescriptionFontSize( Type type )
{
EnsureRegistered( type );
return _descriptionFontSizes.TryGetValue( type, out var fontSize )
? fontSize
: (_descriptionFontSizesByFullName.TryGetValue( GetLookupKey( type ), out fontSize ) ? fontSize : null);
}
public static float? GetDescriptionImageSize( Type type )
{
EnsureRegistered( type );
return _descriptionImageSizes.TryGetValue( type, out var imageSize )
? imageSize
: (_descriptionImageSizesByFullName.TryGetValue( GetLookupKey( type ), out imageSize ) ? imageSize : null);
}
public bool ShouldUpdate { get; protected set; }
public virtual bool UpdateWhenDead => false;
public Player Player { get; set; } = null!;
public int Level { get; set; }
public int MaxLevel { get; set; }
public TimeSince ElapsedTime { get; protected set; }
public RealTimeSince RealTimeSinceLevelUp { get; protected set; }
public RealTimeSince RealTimeSinceLevelDown { get; protected set; }
public string DisplayText { get; protected set; } = "";
public Color DisplayTextColor { get; protected set; }
public float DisplayTextOpacity { get; protected set; }
public float DisplayCooldown { get; protected set; }
public Color DisplayCooldownColor { get; protected set; }
public TimeSince TimeSinceHighlight { get; set; }
public RealTimeSince RealTimeSinceHighlight { get; set; }
public float HighlightDuration { get; protected set; }
public Color HighlightColor { get; protected set; }
public float HighlightOpacity { get; protected set; }
private float _iconAngleOffset;
private float _iconScale;
private bool _iconNeedsLerp;
protected float IconLerpSpeed { get; set; } = 15f;
public float IconAngleOffset
{
get => _iconAngleOffset;
set
{
_iconAngleOffset = value;
if ( MathF.Abs( value ) > 0.001f ) _iconNeedsLerp = true;
}
}
public float IconScale
{
get => _iconScale;
set
{
_iconScale = value;
if ( MathF.Abs( value - 1f ) > 0.001f ) _iconNeedsLerp = true;
}
}
public virtual List<PerkCategory>? IncludedPerkCategories =>
GetType().GetCustomAttribute<PerkAttribute>()?.IncludedCategories?.ToList();
public virtual float? DescriptionLineHeight => null;
public float Importance { get; set; }
public virtual float ImportanceMultiplier => 1f;
public Rarity Rarity { get; set; }
public bool IsCurse { get; set; }
public Perk()
{
}
// called only once, when player gets this perk
public virtual void Start()
{
ElapsedTime = 0f;
ShouldUpdate = false;
DisplayTextColor = Color.White;
DisplayTextOpacity = 2f;
DisplayCooldownColor = new Color( 0f, 0f, 0f, 5f );
TimeSinceHighlight = 999f;
RealTimeSinceHighlight = 999f;
RealTimeSinceLevelUp = 999f;
RealTimeSinceLevelDown = 999f;
HighlightColor = new Color( 1f, 1f, 1f );
HighlightDuration = 0.25f;
HighlightOpacity = 2f;
_iconAngleOffset = 0f;
_iconScale = 1f;
_iconNeedsLerp = false;
if ( IncludedPerkCategories != null )
{
foreach ( var category in IncludedPerkCategories )
Player.IncludePerkCategory( category );
}
Importance = 0f;
var attrib = TypeLibrary.GetType( GetType() ).GetAttribute<PerkAttribute>();
Rarity = attrib.Rarity;
IsCurse = attrib.Curse;
}
// called when player gets this perk or levels it up
public virtual void Refresh()
{
}
public virtual void IncreaseLevel()
{
Level++;
if ( Level == 1 )
Start();
Refresh();
RealTimeSinceLevelUp = 0f;
}
public virtual void DecreaseLevel()
{
Level--;
Refresh();
RealTimeSinceLevelDown = 0f;
}
public void UpdateIcon( float dt )
{
if ( !_iconNeedsLerp ) return;
_iconAngleOffset = MathX.Lerp( _iconAngleOffset, 0f, dt * IconLerpSpeed );
_iconScale = MathX.Lerp( _iconScale, 1f, dt * IconLerpSpeed );
if ( MathF.Abs( _iconAngleOffset ) < 0.001f && MathF.Abs( _iconScale - 1f ) < 0.001f )
{
_iconAngleOffset = 0f;
_iconScale = 1f;
_iconNeedsLerp = false;
}
}
public virtual void Update( float dt )
{
}
public virtual void Remove( bool restart = false )
{
if ( IncludedPerkCategories != null )
{
foreach ( var category in IncludedPerkCategories )
Player.ExcludePerkCategory( category );
}
}
public void Highlight()
{
TimeSinceHighlight = 0f;
RealTimeSinceHighlight = 0f;
}
public virtual void Colliding( Thing other, float percent, float dt ) { }
public virtual void OnRunStart() { }
/// <summary>
/// Before changing direction when blinking.
/// </summary>
/// <param name="dir"></param>
public virtual void OnDashStartedEarly( Vector2 dir ) { }
public virtual void OnDashStarted( Vector2 dir ) { }
public virtual void OnDashFinished( Vector2 dir ) { }
public virtual void OnDashRecharged() { }
public virtual void OnStartReload() { }
public virtual void OnFinishReload() { }
public virtual void OnShoot() { }
public virtual void OnIgnite( Enemy enemy ) { }
public virtual void OnFreeze( Enemy enemy ) { }
public virtual void OnPoison( Enemy enemy ) { }
public virtual void OnFear( Enemy enemy ) { }
public virtual void OnKill( Enemy enemy, DamageType damageType, bool countsAsKill ) { }
public virtual void OnDamageEnemy( Enemy enemy, float damage, DamageType damageType, Vector2 dir, bool isCrit ) { }
public virtual void OnHurt( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, Enemy enemySource, EnemyType enemyType, float previousHealth ) { } // actually lost HP
public virtual void OnHit( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, float force, Enemy enemySource, EnemyType enemyType, float previousHealth ) { } // includes armor lost
public virtual void OnLoseArmor( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, Enemy enemySource ) { }
public virtual bool TryPreventDeath() => false;
public virtual void OnDie() { }
public virtual void OnRevive() { }
public virtual void OnHeal( float amount ) { }
public virtual void OnHealOther( float amount, Player other ) { }
public virtual void OnGainXpCoin( float xp ) { }
public virtual void OnGainHealthpack( float amount ) { }
public virtual void OnPlayerLevelUp() { }
public virtual void OnRerollBefore() { }
public virtual void OnRerollAfter() { }
public virtual void OnGainReroll( int amount ) { }
public virtual void OnBanish() { }
public virtual void OnGainBanish( int amount ) { }
public virtual void OnGainShield() { }
public virtual void OnLoseShield() { }
public virtual void OnLand() { }
public virtual void OnSayChat( string message ) { }
public virtual void OnBulletHitGround( Bullet bullet ) { }
public virtual void OnBulletPierce( Bullet bullet, Thing other ) { }
public virtual void OnBulletBounce( Bullet bullet, Thing other ) { }
public virtual void OnDodged( float damage, DamageType damageType, Vector2 dir, Enemy enemySource ) { }
public virtual void OnSetMinute( int minute ) { }
public virtual void OnAddPerkBefore( TypeDescription type ) { }
public virtual void OnAddPerkAfter( TypeDescription type ) { }
public virtual void OnRemovePerk( TypeDescription type ) { }
public virtual void OnChoosePerk( TypeDescription type ) { }
public virtual void OnPerkChoicesRefreshed( int numUniqueConsumed ) { }
public virtual void OnPerkChoicesAssigned() { }
public virtual void OnTeleport( Vector2 from, Vector2 to ) { }
}