Player partial class handling the perk system. Manages available perks, synchronized network state for UI, choosing/banishing/rerolling perks, applying/ removing perk instances, categories, and related UI messages and stats.
using Sandbox;
using Sandbox.Diagnostics;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
public partial class Player
{
public Dictionary<int, Perk> Perks = new();
[Sync] public NetDictionary<int, int> SyncPerks { get; set; } = new();
public List<int> ValidPerks = new();
public Dictionary<PerkCategory, List<Type>> PerkCategories = new();
private Dictionary<PerkCategory, List<PlayerStat>> _perkCategoryDefaultStats = new();
private Dictionary<PerkCategory, int> _perkCategoryIncludeCounts = new();
public List<TypeDescription> CurrentPerkChoices = new(); // Client-only
public List<bool> CurrentPerkChoicesUnknown = new();
public List<CurseWrongPerkInfo.WrongPerkInfo?> CurrentPerkChoicesWrongInfo = new();
[Sync] public NetList<int> SyncCurrentPerkChoiceIdentities { get; set; } = new();
[Sync] public NetList<int> SyncCurrentPerkChoicesUnknown { get; set; } = new();
[Sync] public NetList<int> SyncCurrentPerkChoiceWrongTypeIdentities { get; set; } = new();
[Sync] public NetList<int> SyncCurrentPerkChoiceWrongAspects { get; set; } = new();
[Sync] public NetList<string> SyncCurrentPerkChoiceDisplayNames { get; set; } = new();
[Sync] public NetList<string> SyncCurrentPerkChoiceDisplayDescriptions { get; set; } = new();
[Sync] public NetList<string> SyncCurrentPerkChoiceDisplayIconPaths { get; set; } = new();
[Sync] public NetList<int> BanishedPerkIdentities { get; set; } = new();
public int NumBanishedPerks => BanishedPerkIdentities.Count;
private List<int> _perkIncreaseOrder = new();
//private bool _addedArmorPerks;
[Sync] public RealTimeSince RealTimeSinceBanishChoice { get; set; }
[Sync] public int BanishChoiceIndex { get; set; }
[Sync] public int PerkHash { get; set; }
[Sync] public int PerkChoiceHash { get; set; }
[Sync] public float AutoRerollTimerProgress { get; set; }
private bool _shouldCheckForAdditionalPerkPoints;
private int _checkForAdditionalPerkPointsFrameDelay;
public int NumPendingUniqueChoices { get; set; }
public List<TypeDescription> FavoritePerks { get; set; } = new(); // only for PerkRandomFavorite
[Sync] public int CurrLevelsUntilCurseChoice { get; set; }
[Sync] public int NumLevelsBetweenCurseChoices { get; set; }
public int NumCursesToChoose { get; set; }
[Sync] public bool IsBeingShownCurseChoices { get; set; }
public bool ShouldShowCurseMessage { get; set; }
public int NumCursesMessagesShown { get; set; }
[Sync] public int AvailableCurseCount { get; set; }
//public const int MAX_PERK_CHOICES = 999;
public int PerkRandomRotationSeed { get; set; }
public bool HasDisplayedPerk( TypeDescription type )
{
if ( type == null )
return false;
return IsProxy
? SyncPerks.ContainsKey( type.Identity )
: HasPerk( type );
}
public int GetDisplayedPerkLevel( TypeDescription type )
{
if ( type == null )
return 0;
if ( IsProxy )
return SyncPerks.TryGetValue( type.Identity, out var level ) ? level : 0;
return GetPerkLevel( type );
}
public int GetDisplayedPerkChoiceCount()
{
return IsProxy ? SyncCurrentPerkChoiceIdentities.Count : CurrentPerkChoices.Count;
}
public TypeDescription GetDisplayedPerkChoice( int index )
{
if ( index < 0 || index >= GetDisplayedPerkChoiceCount() )
return null;
return IsProxy
? PerkManager.IdentityToType( SyncCurrentPerkChoiceIdentities[index] )
: CurrentPerkChoices[index];
}
public bool GetDisplayedPerkChoiceUnknown( int index )
{
if ( index < 0 )
return false;
return IsProxy
? index < SyncCurrentPerkChoicesUnknown.Count && SyncCurrentPerkChoicesUnknown[index] != 0
: index < CurrentPerkChoicesUnknown.Count && CurrentPerkChoicesUnknown[index];
}
public CurseWrongPerkInfo.WrongPerkInfo? GetDisplayedPerkChoiceWrongInfo( int index )
{
if ( index < 0 )
return null;
if ( !IsProxy )
return index < CurrentPerkChoicesWrongInfo.Count ? CurrentPerkChoicesWrongInfo[index] : null;
if ( index >= SyncCurrentPerkChoiceWrongTypeIdentities.Count || index >= SyncCurrentPerkChoiceWrongAspects.Count )
return null;
var wrongTypeIdentity = SyncCurrentPerkChoiceWrongTypeIdentities[index];
if ( wrongTypeIdentity < 0 )
return null;
return new CurseWrongPerkInfo.WrongPerkInfo
{
WrongType = PerkManager.IdentityToType( wrongTypeIdentity ),
Aspect = (CurseWrongPerkInfo.WrongAspect)SyncCurrentPerkChoiceWrongAspects[index]
};
}
public float GetDisplayedAutoRerollTimerProgress()
{
if ( IsProxy )
return AutoRerollTimerProgress;
var autoRerollCurseType = TypeLibrary.GetType( typeof( CurseRerollAuto ) );
if ( !HasPerk( autoRerollCurseType ) )
return 0f;
return GetPerk( autoRerollCurseType ) is CurseRerollAuto autoRerollCurse
? autoRerollCurse.TimerProgress
: 0f;
}
public string GetDisplayedPerkChoiceName( int index, TypeDescription fallbackType, bool isUnknown )
{
if ( isUnknown )
return "Unknown";
if ( IsProxy && index >= 0 && index < SyncCurrentPerkChoiceDisplayNames.Count )
return SyncCurrentPerkChoiceDisplayNames[index] ?? "";
return Perk.GetName( fallbackType?.TargetType ) ?? "";
}
public string GetDisplayedPerkChoiceDescription( int index, TypeDescription fallbackType, int level, bool isChoice, bool isUnknown, Rarity rarity )
{
if ( IsProxy && index >= 0 && index < SyncCurrentPerkChoiceDisplayDescriptions.Count )
return SyncCurrentPerkChoiceDisplayDescriptions[index] ?? "";
if ( !isChoice )
return ResolvePerkDescription( fallbackType?.TargetType, level );
if ( level > PerkManager.GetMaxLevelForRarity( rarity ) )
return "ALREADY MAX!";
if ( isUnknown )
return "Hidden by a curse";
return ResolvePerkDescription( fallbackType?.TargetType, level );
}
public string GetDisplayedPerkChoiceIconPath( int index, TypeDescription fallbackType, bool isUnknown )
{
if ( isUnknown )
return "textures/icons/vector/unknown.png";
if ( IsProxy && index >= 0 && index < SyncCurrentPerkChoiceDisplayIconPaths.Count )
return SyncCurrentPerkChoiceDisplayIconPaths[index] ?? "textures/icons/vector/unknown.png";
return Perk.GetImagePath( fallbackType?.TargetType ) ?? "textures/icons/vector/unknown.png";
}
private static string ResolvePerkDescription( Type type, int level )
{
if ( type == null )
return "";
if ( level <= 1 )
return Perk.GetDescription( type, level ) ?? "";
return Perk.GetUpgradeDescription( type, level )
?? Perk.GetDescription( type, level )
?? "";
}
private void ClearSyncedPerkChoiceState()
{
SyncCurrentPerkChoiceIdentities.Clear();
SyncCurrentPerkChoicesUnknown.Clear();
SyncCurrentPerkChoiceWrongTypeIdentities.Clear();
SyncCurrentPerkChoiceWrongAspects.Clear();
SyncCurrentPerkChoiceDisplayNames.Clear();
SyncCurrentPerkChoiceDisplayDescriptions.Clear();
SyncCurrentPerkChoiceDisplayIconPaths.Clear();
}
private void SyncPerkChoiceState()
{
ClearSyncedPerkChoiceState();
AutoRerollTimerProgress = GetDisplayedAutoRerollTimerProgress();
if ( IsChoosingLevelUpReward )
{
for ( int i = 0; i < CurrentPerkChoices.Count; i++ )
{
var perkType = CurrentPerkChoices[i];
var isUnknown = i < CurrentPerkChoicesUnknown.Count && CurrentPerkChoicesUnknown[i];
SyncCurrentPerkChoiceIdentities.Add( perkType?.Identity ?? -1 );
SyncCurrentPerkChoicesUnknown.Add( isUnknown ? 1 : 0 );
var playerPerkLevel = GetPerkLevel( perkType );
var perkLevel = playerPerkLevel + 1;
var displayNameType = perkType?.TargetType;
var displayDescType = perkType?.TargetType;
var displayIconType = perkType?.TargetType;
if ( i < CurrentPerkChoicesWrongInfo.Count && CurrentPerkChoicesWrongInfo[i].HasValue )
{
var wrongInfo = CurrentPerkChoicesWrongInfo[i].Value;
SyncCurrentPerkChoiceWrongTypeIdentities.Add( wrongInfo.WrongType?.Identity ?? -1 );
SyncCurrentPerkChoiceWrongAspects.Add( (int)wrongInfo.Aspect );
if ( wrongInfo.Aspect == CurseWrongPerkInfo.WrongAspect.Name )
displayNameType = wrongInfo.WrongType?.TargetType ?? displayNameType;
else if ( wrongInfo.Aspect == CurseWrongPerkInfo.WrongAspect.Description )
displayDescType = wrongInfo.WrongType?.TargetType ?? displayDescType;
else if ( wrongInfo.Aspect == CurseWrongPerkInfo.WrongAspect.Icon )
displayIconType = wrongInfo.WrongType?.TargetType ?? displayIconType;
}
else
{
SyncCurrentPerkChoiceWrongTypeIdentities.Add( -1 );
SyncCurrentPerkChoiceWrongAspects.Add( -1 );
}
var displayName = isUnknown ? "Unknown" : (Perk.GetName( displayNameType ) ?? "");
string displayDescription;
if ( perkLevel > PerkManager.GetMaxLevelForRarity( perkType.GetAttribute<PerkAttribute>()?.Rarity ?? Rarity.Common ) )
displayDescription = "ALREADY MAX!";
else if ( isUnknown )
displayDescription = "Hidden by a curse";
else
displayDescription = ResolvePerkDescription( displayDescType, perkLevel );
var displayIconPath = isUnknown
? "textures/icons/vector/unknown.png"
: (Perk.GetImagePath( displayIconType ) ?? "textures/icons/vector/unknown.png");
SyncCurrentPerkChoiceDisplayNames.Add( displayName );
SyncCurrentPerkChoiceDisplayDescriptions.Add( displayDescription );
SyncCurrentPerkChoiceDisplayIconPaths.Add( displayIconPath );
}
}
PerkChoiceHash++;
}
private void UpdatePerkChoiceRuntimeSyncState()
{
var autoRerollTimerProgress = GetDisplayedAutoRerollTimerProgress();
if ( MathF.Abs( AutoRerollTimerProgress - autoRerollTimerProgress ) > 0.0001f )
AutoRerollTimerProgress = autoRerollTimerProgress;
}
void PerksStart()
{
if ( PerkCategories.Count == 0 )
CreatePerkCategories();
}
public void InitPerks()
{
if ( PerkCategories.Count == 0 )
CreatePerkCategories();
ClearAllPerks();
Perks.Clear();
SyncPerks.Clear();
ClearSyncedPerkChoiceState();
BanishedPerkIdentities.Clear();
_perkIncreaseOrder.Clear();
ValidPerks.Clear();
_perkCategoryIncludeCounts.Clear();
foreach ( var type in TypeLibrary.GetTypes<Perk>() )
{
var attrib = type.GetAttribute<PerkAttribute>();
if ( attrib == null )
continue;
if ( attrib.Disabled )
continue;
if ( attrib.IncludedAtStart || attrib.AlwaysOfferDebug )
//if ( attrib.IncludedAtStart )
ValidPerks.Add( PerkManager.TypeToIdentity( type ) );
}
//_addedArmorPerks = false;
_shouldCheckForAdditionalPerkPoints = false;
PerkHash = 0;
PerkChoiceHash = 0;
AutoRerollTimerProgress = 0f;
NumPendingUniqueChoices = 0;
DetermineFavoritePerks();
CurrLevelsUntilCurseChoice = NumLevelsBetweenCurseChoices = 6;
NumCursesToChoose = 0;
IsBeingShownCurseChoices = false;
ShouldShowCurseMessage = false;
NumCursesMessagesShown = 0;
RefreshAvailableCurseCount();
}
public void RefreshAvailableCurseCount()
{
AvailableCurseCount = PerkManager.GetAvailableCurseCount( this );
}
void HandlePerks()
{
//string debug = "";
var dt = Time.Delta;
for ( int i = Perks.Count - 1; i >= 0; i-- )
{
Perk perk = Perks.Values.ElementAt( i );
perk.UpdateIcon( RealTime.Delta );
if ( perk.ShouldUpdate && (!IsDead || perk.UpdateWhenDead) )
perk.Update( dt );
//debug += $"{perk.Title} ({perk.Level})\n";
if ( perk.IsCurse )
continue;
perk.Importance += dt * perk.Level * GetImportanceWeightForRarity( perk.Rarity ) * perk.ImportanceMultiplier;
}
// hack to prevent choice UI from getting messed up (repro: get PerkNumChoicesCantMove, then when seeing OnlyShowIcon choices, level up again, then make a choice - choices will remain small but with cramped descriptions etc)
// later on added the SkipShowingChoicesFrames hack for a similar reason, maybe _shouldCheckForAdditionalPerkPoints isn't necessary anymore
if ( _shouldCheckForAdditionalPerkPoints )
{
if ( _checkForAdditionalPerkPointsFrameDelay > 0 )
{
_checkForAdditionalPerkPointsFrameDelay--;
}
else if ( !IsChoosingLevelUpReward && NumPerkPointsAvailable > 0 )
{
IsChoosingLevelUpReward = true;
RefreshPerkChoices();
RealTimeSinceOfferedChoices = 0f;
}
}
UpdatePerkChoiceRuntimeSyncState();
//DebugOverlay.Text($"{Client.Name}", Position, 0f, float.MaxValue);
}
void GiveRandomPerkZeroChoices( CurseSelection curseSelection )
{
var type = GetRandomPerkType( curseSelection: curseSelection, isReward: false );
Manager.Instance.Chat.AddLocalChatMessage( $"0 perk choices, so got a random {(curseSelection == CurseSelection.OnlyCurses ? "curse" : "perk")}: {Perk.GetRichTextToken( type )}", from: "" );
GivePerkItem( type, dir: Utils.GetRandomVectorInCone( -FacingDir ) );
Manager.Instance.SpawnFloaterText( WorldPosition.WithZ( 65f ), "0 CHOICES!", new Color( 1f, 0.25f, 0.5f ), 1.3f, FloaterType.NegativeMessage );
}
public void ForEachPerk( Action<Perk> action )
{
if ( IsProxy )
return;
var snapshot = Perks.Values.ToList();
for ( int i = snapshot.Count - 1; i >= 0; i-- )
action( snapshot[i] );
}
public void ForEachLoadoutItem( Action<LoadoutItem> action )
{
if ( IsProxy || Manager.HideProgressionSystem )
return;
if ( CurrentGun != null )
action( CurrentGun );
foreach ( var charm in CurrentCharms )
if ( charm != null ) action( charm );
foreach ( var gem in CurrentGems )
action( gem );
}
[Rpc.Owner]
public void AddPerkRpc( int typeIdentity )
{
AddPerk( PerkManager.IdentityToType( typeIdentity ) );
}
public void AddPerk( TypeDescription type )
{
Assert.True( !IsProxy );
Perk perk = null;
var typeIdentity = type.Identity;
ForEachPerk( perk => perk.OnAddPerkBefore( type ) );
ForEachLoadoutItem( item => item.OnAddPerkBefore( type ) );
// first upgrade
if ( Perks.Count == 0 )
IncludePerkCategory( PerkCategory.AfterFirstPerk );
if ( Perks.ContainsKey( typeIdentity ) )
{
perk = Perks[typeIdentity];
if ( perk.Level >= perk.MaxLevel )
{
Manager.Instance.PlaySfxUI( "error2", pitch: 0.55f, volume: 0.9f );
Manager.Instance.SpawnFloaterText( WorldPosition.WithZ( 70f ), "ALREADY MAX!", new Color( 1f, 0.5f, 0.5f ), size: 1.75f, floaterType: FloaterType.NegativeMessage );
Log.Info( $"{perk} already at max level of {perk.MaxLevel}!" );
return;
}
perk.IncreaseLevel();
AddResultsStat( ResultStat.PerkLeveledUp, 1 );
if ( perk.Level >= perk.MaxLevel )
ProgressManager.IncrementStat( ProgressStat.PerkMaxedOut, 1 );
}
else
{
AddResultsStat( ResultStat.NewPerk, 1 );
}
if ( perk == null )
{
perk = PerkManager.CreatePerk( type );
Perks.Add( typeIdentity, perk );
perk.Player = this;
perk.IncreaseLevel();
}
if ( SyncPerks.ContainsKey( typeIdentity ) )
SyncPerks[typeIdentity]++;
else
SyncPerks.Add( typeIdentity, 1 );
if ( _perkIncreaseOrder.Contains( typeIdentity ) )
_perkIncreaseOrder.Remove( typeIdentity );
_perkIncreaseOrder.Add( typeIdentity );
float progress = perk.Level / (float)perk.MaxLevel;
var attrib = type.GetAttribute<PerkAttribute>();
//var pos = WorldPosition.WithZ( 65f );
var OFFSET = 10f;
var pos = WorldPosition.WithZ( 65f ) + new Vector3( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * OFFSET;
pos += (Manager.Instance.CameraContainer.WorldPosition - pos).Normal * Game.Random.Float( 55f, 65f );
Manager.Instance.SpawnPerkFloaterRpc( pos, Perk.GetImagePath( perk.GetType() ), attrib.Rarity, attrib.Curse, progress );
ForEachPerk( perk => perk.OnAddPerkAfter( type ) );
ForEachLoadoutItem( item => item.OnAddPerkAfter( type ) );
PerkHash++;
if( perk.IsCurse )
{
AddResultsStat( ResultStat.CursesGot, 1 );
ProgressManager.IncrementStat( ProgressStat.CursesCollected, 1 );
RefreshAvailableCurseCount();
Manager.Instance.PlaySfxUI( "cursed", pitch: Game.Random.Float(1f, 1.2f), volume: 0.95f );
}
if ( attrib?.Rarity == Rarity.Unique )
ProgressManager.IncrementStat( ProgressStat.UniquePerksCollected, 1 );
ProgressManager.IncrementStat( ProgressStat.PerksCollected, 1 );
}
private void ClearAllPerks()
{
if ( IsProxy )
return;
foreach ( KeyValuePair<int, Perk> pair in Perks )
{
Perk perk = pair.Value;
perk.Remove( restart: true );
}
Perks.Clear();
}
public void AddValidPerk( Type type )
{
int typeIdent = TypeLibrary.GetType( type ).Identity;
if ( !ValidPerks.Contains( typeIdent ) && !BanishedPerkIdentities.Contains( typeIdent ) )
ValidPerks.Add( typeIdent );
}
public void RemoveValidPerk( Type type )
{
int typeIdent = TypeLibrary.GetType( type ).Identity;
if ( ValidPerks.Contains( typeIdent ) )
ValidPerks.Remove( typeIdent );
}
public void IncludePerkCategory( PerkCategory category )
{
if ( _perkCategoryIncludeCounts.ContainsKey( category ) )
{
_perkCategoryIncludeCounts[category]++;
return;
}
AddIncludedPerkCategories( category );
_perkCategoryIncludeCounts[category] = 1;
if ( category == PerkCategory.Fire || category == PerkCategory.Freeze )
{
if ( _perkCategoryIncludeCounts.ContainsKey( PerkCategory.Fire ) && _perkCategoryIncludeCounts.ContainsKey( PerkCategory.Freeze ) )
AddIncludedPerkCategories( PerkCategory.FireAndFreeze );
}
if ( category == PerkCategory.OneHpLeft || category == PerkCategory.Dodge )
{
if ( _perkCategoryIncludeCounts.ContainsKey( PerkCategory.OneHpLeft ) && _perkCategoryIncludeCounts.ContainsKey( PerkCategory.Dodge ) )
AddIncludedPerkCategories( PerkCategory.OneHpLeftAndDodge );
}
}
void AddIncludedPerkCategories( PerkCategory category )
{
if ( PerkCategories.ContainsKey( category ) )
{
foreach ( Type type in PerkCategories[category] )
AddValidPerk( type );
}
if ( _perkCategoryDefaultStats.ContainsKey( category ) )
{
foreach ( PlayerStat statType in _perkCategoryDefaultStats[category] )
AddDefaultValueStat( statType );
}
}
public void ExcludePerkCategory( PerkCategory category )
{
if ( !_perkCategoryIncludeCounts.ContainsKey( category ) )
return;
if ( _perkCategoryIncludeCounts[category] > 1 )
{
_perkCategoryIncludeCounts[category]--;
}
else
{
_perkCategoryIncludeCounts.Remove( category );
RemoveIncludedPerkCategories( category );
}
if ( category == PerkCategory.Fire || category == PerkCategory.Freeze )
{
if ( !_perkCategoryIncludeCounts.ContainsKey( PerkCategory.Fire ) || !_perkCategoryIncludeCounts.ContainsKey( PerkCategory.Freeze ) )
RemoveIncludedPerkCategories( PerkCategory.FireAndFreeze );
}
if ( category == PerkCategory.OneHpLeft || category == PerkCategory.Dodge )
{
if ( !_perkCategoryIncludeCounts.ContainsKey( PerkCategory.OneHpLeft ) || !_perkCategoryIncludeCounts.ContainsKey( PerkCategory.Dodge ) )
RemoveIncludedPerkCategories( PerkCategory.OneHpLeftAndDodge );
}
}
void RemoveIncludedPerkCategories( PerkCategory category )
{
if ( PerkCategories.ContainsKey( category ) )
{
foreach ( Type type in PerkCategories[category] )
RemoveValidPerk( type );
}
if ( _perkCategoryDefaultStats.ContainsKey( category ) )
{
foreach ( PlayerStat statType in _perkCategoryDefaultStats[category] )
RemoveDefaultValueStat( statType );
}
}
public bool HasPerk( TypeDescription type )
{
return Perks.ContainsKey( type.Identity );
}
public Perk GetPerk( TypeDescription type )
{
if ( Perks.ContainsKey( type.Identity ) )
return Perks[type.Identity];
return null;
}
public int GetPerkLevel( TypeDescription type )
{
if ( Perks.ContainsKey( type.Identity ) )
return Perks[type.Identity].Level;
return 0;
}
//public void RemovePerkReqs()
//{
// foreach ( var type in TypeLibrary.GetTypes<Perk>() )
// {
// var attrib = type.GetAttribute<PerkAttribute>();
// if ( attrib == null )
// continue;
// if ( attrib.Disabled )
// continue;
// bool skip = false;
// foreach ( var banishedIdentity in BanishedPerkIdentities )
// {
// TypeDescription banishedType = PerkManager.IdentityToType( banishedIdentity );
// if ( banishedType == type )
// {
// skip = true;
// break;
// }
// }
// if ( skip )
// continue;
// if ( !attrib.IncludedAtStart )
// {
// //Log.Info($"adding valid: {type.TargetType}");
// ValidPerks.Add( PerkManager.TypeToIdentity( type ) );
// }
// }
//}
//[Rpc.Owner]
//public void GiveRandomPerkItemRpc( Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false )
//{
// GiveRandomPerkItem( rarity, curseSelection, forceToCollect );
//}
//public void GiveRandomPerkItem( Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false )
//{
// List<TypeDescription> perkTypes = PerkManager.GetRandomPerks( this, 1, rarity, curseSelection, isChoice: false );
// if ( perkTypes.Count == 0 )
// return;
// var typeIdentity = PerkManager.TypeToIdentity( perkTypes.FirstOrDefault() );
// var playerToTarget = forceToCollect ? this : null;
// Manager.Instance.SpawnPerkItemRpc( typeIdentity, Position2D, Utils.GetRandomVectorInCone( -FacingDir ), playerToTarget );
//}
[Rpc.Owner]
public void GiveRandomPerkItemRpc( Vector2 pos, Vector2 dir, Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false, bool isReward = false )
{
List<TypeDescription> perkTypes = PerkManager.GetRandomPerks( this, numPerks: 1, rarity, curseSelection, isChoice: false, isReward: isReward );
if ( perkTypes.Count == 0 )
return;
var typeIdentity = PerkManager.TypeToIdentity( perkTypes.FirstOrDefault() );
var playerToTarget = forceToCollect ? this : null;
Manager.Instance.SpawnPerkItemRpc( typeIdentity, pos, dir, playerToTarget );
}
public void GiveRandomPerkItemWithMessage( TypeDescription sourcePerkType, Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false, bool isReward = false )
{
var message = $"Got a random {(curseSelection == CurseSelection.OnlyCurses ? "curse" : "perk")}:";
GiveRandomPerkItemWithMessage( sourcePerkType, message, rarity, curseSelection, forceToCollect, isReward );
}
//[Rpc.Owner]
//public void GiveRandomPerkItemWithMessageRpc( TypeDescription sourcePerkType, string message, Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false, bool isReward = false )
//{
// GiveRandomPerkItemWithMessage( sourcePerkType, message, rarity, curseSelection, forceToCollect, isReward );
//}
public void GiveRandomPerkItemWithMessage( TypeDescription sourcePerkType, string message, Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool forceToCollect = false, bool isReward = false )
{
var type = GetRandomPerkType( rarity, curseSelection, isReward: isReward );
if ( type == null )
return;
if ( !string.IsNullOrEmpty( message ) )
{
var sourcePerkToken = sourcePerkType != null ? $"{Perk.GetRichTextToken( sourcePerkType )} " : "";
Manager.Instance.Chat.AddLocalChatMessage( $"{sourcePerkToken}{message} {Perk.GetRichTextToken( type )}", from: "" );
}
GivePerkItem( type, dir: Utils.GetRandomVectorInCone( -FacingDir ), forceToCollect );
}
public TypeDescription GetRandomPerkType( Rarity rarity = Rarity.None, CurseSelection curseSelection = CurseSelection.NoCurses, bool isChoice = false, List<TypeDescription> notAllowed = null, bool isReward = false )
{
List<TypeDescription> perkTypes = PerkManager.GetRandomPerks( this, 1, rarity, curseSelection, isChoice, notAllowed, isReward );
if ( perkTypes.Count > 0 )
return perkTypes.FirstOrDefault();
return null;
}
public bool GiveRandomExistingPerk( Type ignoreType = null, bool showMessage = false, TypeDescription perkType = null, CurseSelection curseSelection = CurseSelection.NoCurses )
{
List<TypeDescription> validTypes = new();
foreach ( var (_, perk) in Perks )
{
Type type = perk.GetType();
if ( type != null && type == ignoreType )
continue;
if ( perk.Level >= perk.MaxLevel )
continue;
if ( perk is PerkBanishOtherChoices )
continue;
if( curseSelection == CurseSelection.NoCurses && perk.IsCurse )
continue;
if ( curseSelection == CurseSelection.OnlyCurses && !perk.IsCurse )
continue;
validTypes.Add( TypeLibrary.GetType( type ) );
}
if ( validTypes.Count > 0 )
{
var type = validTypes[Game.Random.Int( 0, validTypes.Count - 1 )];
GivePerkItem( type, dir: Utils.GetRandomVectorInCone( -FacingDir ) );
if ( showMessage )
{
var sourcePerkToken = perkType != null ? $"{Perk.GetRichTextToken( perkType )} " : "";
Manager.Instance.Chat.AddLocalChatMessage( $"{sourcePerkToken}Got a random existing perk: {Perk.GetRichTextToken( type )}", from: "" );
}
return true;
}
return false;
}
public TypeDescription GetRandomExistingPerkType( Type ignoreType = null )
{
List<TypeDescription> validTypes = new();
foreach ( var (_, perk) in Perks )
{
Type type = perk.GetType();
if ( type != null && type == ignoreType )
continue;
validTypes.Add( TypeLibrary.GetType( type ) );
}
if ( validTypes.Count > 0 )
return validTypes[Game.Random.Int( 0, validTypes.Count - 1 )];
return null;
}
public void GivePerkItem( TypeDescription perkType, Vector2 dir, bool forceToCollect = false )
{
var playerToTarget = forceToCollect ? this : null;
Manager.Instance.SpawnPerkItemRpc( PerkManager.TypeToIdentity( perkType ), Position2D, dir, playerToTarget );
DodgeDuckRpc( dir, time: Game.Random.Float( 0.05f, 0.07f ) );
}
//public void GivePerkItemUIChoice( TypeDescription perkType, Vector2 dir )
//{
// ForEachPerk( perk => perk.OnChoosePerk( perkType ) );
// GivePerkItem( perkType, dir );
// RefreshAfterChoosingPerk();
//}
public void RefreshPerkChoices()
{
CurrentPerkChoices.Clear();
var numChoices = DetermineNumPerkChoicesToSee();
if ( numChoices <= 0 )
{
GiveRandomPerkZeroChoices( curseSelection: IsBeingShownCurseChoices ? CurseSelection.OnlyCurses : CurseSelection.NoCurses );
RefreshAfterChoosingPerk();
return;
}
//numChoices = Math.Min( numChoices, MAX_PERK_CHOICES );
CurrentPerkChoices = PerkManager.GetRandomPerks( this, numChoices, Rarity.None, curseSelection: IsBeingShownCurseChoices ? CurseSelection.OnlyCurses : CurseSelection.NoCurses, isChoice: true );
// Satisfy pending Unique choices (PerkHurtUniqueChoice)
int numUniqueConsumed = 0;
if ( !IsBeingShownCurseChoices && NumPendingUniqueChoices > 0 )
{
// Naturally-occurring Unique perks in the choices satisfy pending counts
foreach ( var choice in CurrentPerkChoices )
{
if ( NumPendingUniqueChoices <= 0 ) break;
var attrib = choice.GetAttribute<PerkAttribute>();
if ( attrib?.Rarity == Rarity.Unique )
{
NumPendingUniqueChoices--;
numUniqueConsumed++;
}
}
// Replace non-Unique slots for any remaining pending counts
int replaceIndex = 0;
while ( NumPendingUniqueChoices > 0 && replaceIndex < CurrentPerkChoices.Count )
{
var slotAttrib = CurrentPerkChoices[replaceIndex].GetAttribute<PerkAttribute>();
if ( slotAttrib?.Rarity == Rarity.Unique )
{
replaceIndex++;
continue;
}
var notAllowedUnique = new List<TypeDescription>( CurrentPerkChoices );
notAllowedUnique.RemoveAt( replaceIndex );
var uniqueResult = PerkManager.GetRandomPerks( this, 1, Rarity.Unique, CurseSelection.NoCurses, isChoice: false, notAllowedUnique );
if ( uniqueResult.Count > 0 )
CurrentPerkChoices[replaceIndex] = uniqueResult[0];
NumPendingUniqueChoices--;
numUniqueConsumed++;
replaceIndex++;
}
}
if ( numUniqueConsumed > 0 )
ForEachPerk( perk => perk.OnPerkChoicesRefreshed( numUniqueConsumed ) );
ForEachLoadoutItem( item => item.OnPerkChoicesRefreshed( numUniqueConsumed ) );
CurrentPerkChoicesUnknown.Clear();
for ( int i = 0; i < CurrentPerkChoices.Count; i++ )
CurrentPerkChoicesUnknown.Add( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.CurseUnknownPerkChance] );
CurrentPerkChoicesWrongInfo.Clear();
CurseWrongPerkInfo.PopulateWrongInfo( this, CurrentPerkChoicesWrongInfo );
CanInteractWithChoices = true;
SyncPerkChoiceState();
//RealTimeSinceRefreshChoices = 0f;
//RealTimeSinceOfferedChoices = 0f;
Manager.Instance.SkipShowingChoicesFrames = 1;
// todo: handle this better
if ( CurrentPerkChoices.Count() == 0 )
IsChoosingLevelUpReward = false;
if ( !IsChoosingLevelUpReward )
SyncPerkChoiceState();
ForEachPerk( perk => perk.OnPerkChoicesAssigned() );
}
public void AddPerkUIChoice( TypeDescription type )
{
Sandbox.Services.Stats.Increment( Manager.GetPerkStatString( StatType.PerkChosen, type ), 1 );
foreach ( var choicePerkType in CurrentPerkChoices )
{
if ( choicePerkType.FullName.Equals( type.FullName ) )
continue;
Sandbox.Services.Stats.Increment( Manager.GetPerkStatString( StatType.PerkIgnored, choicePerkType ), 1 );
}
ForEachPerk( perk => perk.OnChoosePerk( type ) );
ForEachLoadoutItem( item => item.OnChoosePerk( type ) );
AddPerk( type );
RefreshAfterChoosingPerk();
}
public void SubmitPerkStatsOnReroll()
{
foreach ( var choicePerkType in CurrentPerkChoices )
{
Sandbox.Services.Stats.Increment( Manager.GetPerkStatString( StatType.PerkIgnored, choicePerkType ), 1 );
}
}
public void RefreshAfterChoosingPerk()
{
ReimburseWaitingRerolls();
HoveredPerkChoiceSlot = -1;
IsChoosingLevelUpReward = false;
RealTimeSinceChosePerk = 0f;
// handle curses
if ( Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses )
{
// curses don't count as one of your perk points
if ( IsBeingShownCurseChoices )
NumCursesToChoose--;
else
NumPerkPointsAvailable--;
CurrLevelsUntilCurseChoice--;
if ( CurrLevelsUntilCurseChoice <= 0 )
{
// only queue a curse choice if there are available curses
if ( AvailableCurseCount > 0 )
NumCursesToChoose++;
CurrLevelsUntilCurseChoice = NumLevelsBetweenCurseChoices;
}
}
else
{
NumPerkPointsAvailable--;
}
IsBeingShownCurseChoices = NumCursesToChoose > 0 && AvailableCurseCount > 0;
ShouldShowCurseMessage = true;
if ( NumPerkPointsAvailable > 0 )
{
_shouldCheckForAdditionalPerkPoints = true;
_checkForAdditionalPerkPointsFrameDelay = 1;
if ( IsBeingShownCurseChoices )
ShowCurseMessage();
}
SyncPerkChoiceState();
}
public void ShowCurseMessage()
{
List<string> potentialMessages = new()
{
"It's time to choose a curse...",
"A curse awaits you...",
"Choose a curse...",
"Prepare for a curse...",
};
var message = potentialMessages[Game.Random.Int( 0, potentialMessages.Count - 1 )];
Manager.Instance.Chat.AddLocalChatMessage( message, from: "" );
Manager.Instance.PlaySfxUI( "curse_show_choices", pitch: Utils.Map( Level, 1, 50, 1.2f, 1.5f ), volume: 0.95f );
ShouldShowCurseMessage = false;
NumCursesMessagesShown++;
}
public void BanishPerkUIChoice( TypeDescription type )
{
BanishPerk( type );
NumBanishAvailable--;
IsBanishMode = false;
ReimburseWaitingRerolls();
BanishExistingPerkChoice( type.Identity );
//RealTimeSinceRefreshChoices = 0f;
//RealTimeSinceOfferedChoices = 0f;
}
public void BanishPerk( TypeDescription type )
{
RemoveValidPerk( type.TargetType );
var typeIdent = type.Identity;
if ( !BanishedPerkIdentities.Contains( typeIdent ) )
BanishedPerkIdentities.Add( typeIdent );
ForEachPerk( perk => perk.OnBanish() );
ForEachLoadoutItem( item => item.OnBanish() );
AddResultsStat( ResultStat.NumTimesBanish, 1 );
ProgressManager.IncrementStat( ProgressStat.PerksBanished, 1 );
}
public void BanishExistingPerkChoice( int typeIdentity )
{
if ( CurrentPerkChoices == null )
return;
TypeDescription banishedType = PerkManager.IdentityToType( typeIdentity );
List<TypeDescription> notAllowed = new();
foreach ( var perkType in CurrentPerkChoices )
notAllowed.Add( perkType );
for ( int i = CurrentPerkChoices.Count - 1; i >= 0; i-- )
{
var currPerkType = CurrentPerkChoices[i];
if ( banishedType == currPerkType )
{
CurrentPerkChoices.RemoveAt( i );
List<TypeDescription> perkTypes = PerkManager.GetRandomPerks( this, 1, Rarity.None, curseSelection: IsBeingShownCurseChoices ? CurseSelection.OnlyCurses : CurseSelection.NoCurses, isChoice: true, notAllowed );
if ( perkTypes.Count > 0 )
{
var type = perkTypes.FirstOrDefault();
RealTimeSinceBanishChoice = 0f;
BanishChoiceIndex = i;
CurrentPerkChoices.Insert( i, type );
}
ForEachPerk( perk => perk.OnPerkChoicesAssigned() );
if ( i < CurrentPerkChoicesUnknown.Count )
CurrentPerkChoicesUnknown[i] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.CurseUnknownPerkChance];
if ( i < CurrentPerkChoicesWrongInfo.Count )
CurrentPerkChoicesWrongInfo[i] = i < CurrentPerkChoices.Count
? CurseWrongPerkInfo.GetWrongInfoForChoice( this, CurrentPerkChoices[i] )
: null;
break;
}
}
CanInteractWithChoices = true;
SyncPerkChoiceState();
}
public void BanishAllCurrentChoices()
{
foreach ( var perkType in CurrentPerkChoices )
{
if ( perkType == TypeLibrary.GetType( typeof( PerkBanishOtherChoices ) ) )
continue;
BanishPerk( perkType );
}
//RefreshPerkChoices();
}
public void RerollSinglePerkChoice( TypeDescription typeToReroll )
{
if ( CurrentPerkChoices == null )
return;
List<TypeDescription> notAllowed = new();
foreach ( var perkType in CurrentPerkChoices )
notAllowed.Add( perkType );
for ( int i = CurrentPerkChoices.Count - 1; i >= 0; i-- )
{
var currPerkType = CurrentPerkChoices[i];
if ( typeToReroll == currPerkType )
{
CurrentPerkChoices.RemoveAt( i );
List<TypeDescription> perkTypes = PerkManager.GetRandomPerks( this, 1, Rarity.None, curseSelection: IsBeingShownCurseChoices ? CurseSelection.OnlyCurses : CurseSelection.NoCurses, isChoice: true, notAllowed );
if ( perkTypes.Count > 0 )
{
var type = perkTypes.FirstOrDefault();
RealTimeSinceBanishChoice = 0f;
BanishChoiceIndex = i;
CurrentPerkChoices.Insert( i, type );
ForEachPerk( perk => perk.OnPerkChoicesAssigned() );
PerkChoiceHash++;
}
if ( i < CurrentPerkChoicesUnknown.Count )
CurrentPerkChoicesUnknown[i] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.CurseUnknownPerkChance];
if ( i < CurrentPerkChoicesWrongInfo.Count )
CurrentPerkChoicesWrongInfo[i] = i < CurrentPerkChoices.Count
? CurseWrongPerkInfo.GetWrongInfoForChoice( this, CurrentPerkChoices[i] )
: null;
break;
}
}
SyncPerkChoiceState();
}
public TypeDescription GetLastPerkType()
{
if ( _perkIncreaseOrder.Count == 0 )
return null;
return TypeLibrary.GetTypeByIdent( _perkIncreaseOrder[_perkIncreaseOrder.Count - 1] );
}
public void RemovePerk( TypeDescription type )
{
var typeIdentity = type.Identity;
if ( !Perks.ContainsKey( typeIdentity ) )
return;
Perk perk = Perks[typeIdentity];
SpawnRemovePerkFloater( perk, type );
Perks.Remove( typeIdentity );
SyncPerks.Remove( typeIdentity );
RemoveModifiers( perk );
if ( _perkIncreaseOrder.Contains( typeIdentity ) )
_perkIncreaseOrder.Remove( typeIdentity );
var wasCurse = perk.IsCurse;
perk.Remove();
PerkHash++;
if ( Perks.Count == 0 )
ExcludePerkCategory( PerkCategory.AfterFirstPerk );
ForEachPerk( perk => perk.OnRemovePerk( type ) );
ForEachLoadoutItem( item => item.OnRemovePerk( type ) );
if ( wasCurse )
RefreshAvailableCurseCount();
}
void SpawnRemovePerkFloater( Perk perk, TypeDescription type, bool noProgress = false )
{
float progress = noProgress ? 0f : (perk.Level / (float)perk.MaxLevel);
var attrib = type.GetAttribute<PerkAttribute>();
var OFFSET = 10f;
var pos = WorldPosition.WithZ( 25f ) + new Vector3( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * OFFSET;
pos += (Manager.Instance.CameraContainer.WorldPosition - pos).Normal * Game.Random.Float( 55f, 65f );
Manager.Instance.SpawnPerkFloaterRpc( pos, Perk.GetImagePath( perk.GetType() ), attrib.Rarity, attrib.Curse, progress, wasRemoved: true );
}
public void LevelDownPerk( TypeDescription type )
{
var typeIdentity = type.Identity;
if ( !Perks.ContainsKey( typeIdentity ) )
return;
Perk perk = Perks[typeIdentity];
if ( perk.Level <= 1 )
{
RemovePerk( type );
return;
}
SpawnRemovePerkFloater( perk, type, noProgress: true );
perk.DecreaseLevel();
SyncPerks[typeIdentity]--;
PerkHash++;
if ( perk.IsCurse )
RefreshAvailableCurseCount();
}
public void LevelEarliestPerk()
{
foreach ( var (_, perk) in Perks )
{
if ( perk.Level >= perk.MaxLevel )
continue;
if ( perk is PerkIncreaseEarliestPerk || perk is PerkBanishOtherChoices )
continue;
var typeIdent = TypeLibrary.GetTypeIdent( perk.GetType() );
if ( BanishedPerkIdentities.Contains( typeIdent ) )
continue;
GivePerkItem( TypeLibrary.GetType( perk.GetType() ), dir: Utils.GetRandomVector() );
return;
}
Manager.Instance.PlaySfxUI( "error2", pitch: 0.5f, volume: 0.95f );
Manager.Instance.SpawnFloaterText( WorldPosition.WithZ( 65f ), "FAILED!", new Color( 1f, 0.5f, 0.5f ), 1.3f, FloaterType.NegativeMessage );
}
public void LevelPreviousPerk()
{
for ( int i = _perkIncreaseOrder.Count - 1; i >= 0; i-- )
{
var typeIdentity = _perkIncreaseOrder[i];
if ( !Perks.ContainsKey( typeIdentity ) )
continue;
Perk perk = Perks[typeIdentity];
if ( perk.Level >= perk.MaxLevel )
continue;
if ( perk is PerkDoubleDown || perk is PerkBanishOtherChoices )
continue;
if ( perk.IsCurse )
continue;
GivePerkItem( TypeLibrary.GetTypeByIdent( typeIdentity ), dir: Utils.GetRandomVectorInCone( -FacingDir ) );
return;
}
Manager.Instance.PlaySfxUI( "error2", pitch: 0.5f, volume: 0.95f );
Manager.Instance.SpawnFloaterText( WorldPosition.WithZ( 65f ), "FAILED!", new Color( 1f, 0.5f, 0.5f ), 1.3f, FloaterType.NegativeMessage );
}
[Rpc.Owner]
public void SetMinute( int minute )
{
ForEachPerk( perk => perk.OnSetMinute( minute ) );
ForEachLoadoutItem( item => item.OnSetMinute( minute ) );
}
[Rpc.Owner( NetFlags.Unreliable )]
public void HighlightPerkRpc( int typeIdentity )
{
HighlightPerk( PerkManager.IdentityToType( typeIdentity ) );
}
public void HighlightPerk( TypeDescription perkType )
{
Assert.True( !IsProxy );
var perk = GetPerk( perkType );
if ( perk != null )
{
perk.Highlight();
perk.IconScale = Game.Random.Float( 1.2f, 1.3f );
perk.IconAngleOffset = Game.Random.Float( 10f, 20f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
}
}
public Dictionary<int, int> GetImportantPerks()
{
var importantPerks = new Dictionary<int, int>();
// Get the 3 perks with highest Importance values
var topPerks = Perks
.OrderByDescending( pair => pair.Value.Importance )
.Take( 3 );
foreach ( var pair in topPerks )
{
importantPerks[pair.Key] = pair.Value.Level;
}
return importantPerks;
}
public static float GetImportanceWeightForRarity( Rarity rarity )
{
switch ( rarity )
{
case Rarity.Common: default: return 1f;
case Rarity.Uncommon: return (7f / 6f);
case Rarity.Rare: return (7f / 5f);
case Rarity.Epic: return (7f / 4.1f);
case Rarity.Mythic: return (7f / 3.2f);
case Rarity.Legendary: return (7f / 2.3f);
case Rarity.Unique: return (7f / 1.3f);
}
}
void CreatePerkCategories()
{
PerkCategories.Clear();
_perkCategoryDefaultStats.Clear();
_perkCategoryIncludeCounts.Clear();
PerkCategories.Add( PerkCategory.AfterFirstPerk, new List<Type>()
{
typeof( PerkDoubleDown ),
typeof( PerkIncreaseEarliestPerk ),
typeof( PerkRestartChoices ),
typeof( PerkRandomExisting ),
typeof( PerkCleanse ),
typeof( PerkMaxAmmoDownside ),
typeof( PerkHandoutsRemove ),
} );
PerkCategories.Add( PerkCategory.Fire, new List<Type>()
{
typeof( PerkFireDamage ),
typeof( PerkFireLifetime ),
typeof( PerkFireSpread ),
typeof( PerkFireStack ),
} );
_perkCategoryDefaultStats.Add( PerkCategory.Fire, new List<PlayerStat>()
{
PlayerStat.FireDamage,
PlayerStat.FireLifetime,
} );
PerkCategories.Add( PerkCategory.Freeze, new List<Type>()
{
typeof( PerkFreezeLifetime ),
typeof( PerkFreezeShards ),
typeof( PerkFreezeStrength ),
typeof( PerkFreezeDoubleDamage ),
} );
_perkCategoryDefaultStats.Add( PerkCategory.Freeze, new List<PlayerStat>()
{
PlayerStat.FreezeStrengthDisplay,
PlayerStat.FreezeLifetime,
} );
PerkCategories.Add( PerkCategory.FireAndFreeze, new List<Type>()
{
typeof( PerkFreezeBurn ),
} );
PerkCategories.Add( PerkCategory.Poison, new List<Type>()
{
typeof( PerkPoisonDamage ),
typeof( PerkPoisonFinishDamage ),
typeof( PerkPoisonSpread ),
typeof( PerkPoisonFlammable ),
typeof( PerkPoisonDoubleDmgLoseHp ),
typeof( PerkPoisonIncreaseNearby ),
} );
_perkCategoryDefaultStats.Add( PerkCategory.Poison, new List<PlayerStat>()
{
PlayerStat.PoisonDamage,
} );
PerkCategories.Add( PerkCategory.Fear, new List<Type>()
{
typeof( PerkFearDamage ),
typeof( PerkFearBomb ),
typeof( PerkFearBomb ),
typeof( PerkFearDrain ),
typeof( PerkFearLifetime ),
//typeof(FearVoodooUpgrade)
typeof( PerkFearSharedPain ),
typeof( PerkFearReduceDamage ),
} );
_perkCategoryDefaultStats.Add( PerkCategory.Fear, new List<PlayerStat>()
{
PlayerStat.FearLifetime,
} );
PerkCategories.Add( PerkCategory.SelfDmg, new List<Type>()
{
typeof( PerkSelfDmgReduction ),
typeof( PerkSelfDmgAoe ),
typeof( PerkSelfDmgHealthPack ),
typeof( PerkSelfDamageInvuln ),
typeof( PerkSelfDmgDash ),
} );
PerkCategories.Add( PerkCategory.Dodge, new List<Type>()
{
typeof( PerkDodgeReload ),
typeof( PerkDodgeAoe ),
typeof( PerkDodgeHeal ),
typeof( PerkDodgeClipBonus ),
typeof( PerkDodgeHpDamage ),
} );
PerkCategories.Add( PerkCategory.Explosion, new List<Type>()
{
typeof( PerkExplosionDamage ),
typeof( PerkExplosionSize ),
typeof( PerkExplosionMagnet ),
typeof( PerkExplosionFear ),
typeof( PerkExplosionInward ),
} );
PerkCategories.Add( PerkCategory.BulletBounce, new List<Type>()
{
typeof( PerkBounceDamage ),
typeof( PerkBounceResetLifetime ),
typeof( PerkBounceTarget ),
typeof( PerkBulletBounceCopy ),
} );
PerkCategories.Add( PerkCategory.BulletPierce, new List<Type>()
{
typeof( PerkBulletPierceCopy ),
} );
PerkCategories.Add( PerkCategory.Armor, new List<Type>()
{
typeof( PerkArmorRerollCost ),
typeof( PerkArmorShoot ),
typeof( PerkArmorSpeed ),
typeof( PerkArmorDamageReduction ),
} );
PerkCategories.Add( PerkCategory.Shield, new List<Type>()
{
typeof( PerkShieldBreakHeal ),
typeof( PerkShieldBreakAoe ),
typeof( PerkShieldMinDmg ),
} );
PerkCategories.Add( PerkCategory.Radiation, new List<Type>()
{
typeof( PerkRadiationDelay ),
typeof( PerkRadiationHeal ),
typeof( PerkRadiationRepel ),
} );
PerkCategories.Add( PerkCategory.Landmine, new List<Type>()
{
typeof( PerkLandmineBullets ),
typeof( PerkLandmineRepelPlayers ),
} );
PerkCategories.Add( PerkCategory.Aoe, new List<Type>()
{
typeof( PerkRadiusMultiplier ),
} );
PerkCategories.Add( PerkCategory.NumProjectiles, new List<Type>()
{
typeof( PerkReduceSpread ),
} );
PerkCategories.Add( PerkCategory.Bomb, new List<Type>()
{
typeof( PerkBombSticky ),
} );
PerkCategories.Add( PerkCategory.Blink, new List<Type>()
{
typeof( PerkBlinkCursor ),
} );
PerkCategories.Add( PerkCategory.Boomerang, new List<Type>()
{
typeof( PerkBoomerangHurtSelf ),
typeof( PerkBoomerangBounceSelf ),
} );
PerkCategories.Add( PerkCategory.Punch, new List<Type>()
{
typeof( PerkPunchBullets ),
typeof( PerkPunchReload ),
typeof( PerkPunchForce ),
typeof( PerkPunchLifesteal ),
// todo: add punch dmg default stat
} );
PerkCategories.Add( PerkCategory.OneHpLeft, new List<Type>()
{
typeof( PerkOnly1HpAttackSpeed ),
typeof( PerkOnly1HpMoveSpeed ),
typeof( PerkOnly1HpXpGain ),
typeof( PerkOnly1HpBulletHoming ),
} );
PerkCategories.Add( PerkCategory.OneHpLeftAndDodge, new List<Type>()
{
typeof( PerkOnly1HpDodge ),
} );
PerkCategories.Add( PerkCategory.Jump, new List<Type>()
{
typeof( PerkJumpLandDamage ),
typeof( PerkJumpLandArcBullets ),
} );
PerkCategories.Add( PerkCategory.Artillery, new List<Type>()
{
typeof( PerkArtillerySupplies ),
} );
PerkCategories.Add( PerkCategory.OrbiterBlade, new List<Type>()
{
typeof( PerkOrbiterBladeStopTime ),
//typeof( PerkOrbiterBladeHitInterval ),
typeof( PerkOrbiterBladeSpeed ),
typeof( PerkOrbiterBladeBullet ),
typeof( PerkOrbiterBladeConsecutiveDmg ),
} );
PerkCategories.Add( PerkCategory.ArcBullets, new List<Type>()
{
typeof( PerkArcBulletsBounceGround ),
} );
}
void DetermineFavoritePerks()
{
FavoritePerks.Clear();
Dictionary<TypeDescription, float> favoritePerks = new();
foreach ( var type in TypeLibrary.GetTypes<Perk>() )
{
var attrib = type.GetAttribute<PerkAttribute>();
if ( attrib == null )
continue;
if ( attrib.Disabled )
continue;
if ( attrib.Curse )
continue;
var timesChosen = (int)Sandbox.Services.Stats.LocalPlayer.Get( Manager.GetPerkStatString( StatType.PerkChosen, type ) ).Sum;
var timesIgnored = (int)Sandbox.Services.Stats.LocalPlayer.Get( Manager.GetPerkStatString( StatType.PerkIgnored, type ) ).Sum;
if ( timesChosen >= 2 )
{
var percent = MathX.Clamp( timesChosen / (float)(timesChosen + timesIgnored), 0f, 1f );
favoritePerks[type] = percent;
}
}
FavoritePerks.AddRange( favoritePerks.OrderByDescending( x => x.Value ).Take( 10 ).Select( x => x.Key ) );
}
}