// Base perk.
Class Ext_PerkBase extends Info
	NotPlaceable
	Abstract
	Config(ServerExt)
	DependsOn(ExtWebAdmin_UI);

var array<FWebAdminConfigInfo> WebConfigs;

var ExtPerkManager PerkManager;
var Controller PlayerOwner;

var() localized string PerkName;
var() Texture2D PerkIcon;
var() class<KFPerk> BasePerk; // KF perk that this perk is based on.
var() class<KFWeapon> PrimaryMelee,PrimaryWeapon;
var() class<KFWeaponDefinition> PrimaryWeaponDef,SecondaryWeaponDef,KnifeWeaponDef,GrenadeWeaponDef;
var() class<KFProj_Grenade> GrenadeClass,PerkGrenade,SuperGrenade;
var() int HealExpUpNum,WeldExpUpNum; // Efficiency of healing and welding XP up.

// For trader.
var() array<class<KFWeaponDefinition> > AutoBuyLoadOutPath;

// Config init stuff.
var config int ConfigVersion;
var const int CurrentConfigVer;

// Variables.
var config int FirstLevelExp, // How much EXP needed for first level.
				LevelUpExpCost, // How much EXP needed for every level up.
				LevelUpIncCost, // How much EXP increase needed for each level up.
				MinimumLevel,
				MaximumLevel,
				StarPointsPerLevel,
				MinLevelForPrestige, // Minimum level required for perk prestige.
				PrestigeSPIncrease, // Starpoint increase per prestige levelup.
				MaxPrestige,
				MinimalDataLevel; // Maximum prestige level.
var config float PrestigeXPReduce; // Amount of XP cost is reduced for each prestige.
var config array<string> TraitClasses;

var array<float> Modifiers;

var int CurrentLevel, // Current level player is on.
		CurrentEXP, // Current amount of EXP user has.
		NextLevelEXP, // Experience needed for next level.
		CurrentSP, // Current amount of star points.
		LastLevelEXP, // Number of XP was needed for last level.
		CurrentPrestige; // Current prestige level.

struct FPerkStat
{
	var config int MaxValue,CostPerValue;
	var config float Progress;
	var config name StatType;
	var transient int CurrentValue,OldValue;
	var transient float DisplayValue;
	var transient string UIName;
};
var config array<FPerkStat> PerkStats;

struct FDefPerkStat
{
	var int MaxValue,CostPerValue;
	var float Progress;
	var name StatType;
	var bool bHiddenConfig; // Hide this config by default.
};
var() array<FDefPerkStat> DefPerkStats;
var() array< class<Ext_TraitBase> > DefTraitList;

struct FPlayerTrait
{
	var class<Ext_TraitBase> TraitType;
	var byte CurrentLevel;
	var Ext_TraitDataStore Data;
};
var array<FPlayerTrait> PerkTraits;

// Server -> Client replication variables
var byte RepState;
var int RepIndex;
var transient float NextAuthTime;

var int ToxicDartDamage;
var byte EnemyHealthRange;
var() array<float> EnemyDistDraw;

var bool bOwnerNetClient,bClientAuthorized,bPerkNetReady,bHasNightVision,bCanBeGrabbed,bExplosiveWeld,bExplodeOnContact,bNapalmFire,bFireExplode,bToxicDart,bTacticalReload,bHeavyArmor,bHasSWATEnforcer;

var localized string StatSpeed;
var localized string StatDamage;
var localized string StatRecoil;
var localized string StatSpread;
var localized string StatRate;
var localized string StatReload;
var localized string StatHealth;
var localized string StatKnockDown;
var localized string StatWelder;
var localized string StatHeal;
var localized string StatMag;
var localized string StatSpare;
var localized string StatOffDamage;
var localized string StatSelfDamage;
var localized string StatArmor;
var localized string StatPoisonDmg;
var localized string StatSonicDmg;
var localized string StatFireDmg;
var localized string StatAllDmg;
var localized string StatHeadDamage;
var localized string StatHealRecharge;

reliable client simulated function string UIName(FDefPerkStat DefPerkStat)
{
	switch (DefPerkStat.StatType)
	{
		case name("Speed"):		return StatSpeed;
		case name("Damage"):	   return StatDamage;
		case name("Recoil"):	   return StatRecoil;
		case name("Spread"):	   return StatSpread;
		case name("Rate"):		 return StatRate;
		case name("Reload"):	   return StatReload;
		case name("Health"):	   return StatHealth;
		case name("KnockDown"):	return StatKnockDown;
		case name("Welder"):	   return StatWelder;
		case name("Heal"):		 return StatHeal;
		case name("Mag"):		  return StatMag;
		case name("Spare"):		return StatSpare;
		case name("OffDamage"):	return StatOffDamage;
		case name("SelfDamage"):   return StatSelfDamage;
		case name("Armor"):		return StatArmor;
		case name("PoisonDmg"):	return StatPoisonDmg;
		case name("SonicDmg"):	 return StatSonicDmg;
		case name("FireDmg"):	  return StatFireDmg;
		case name("AllDmg"):	   return StatAllDmg;
		case name("HeadDamage"):   return StatHeadDamage;
		case name("HealRecharge"): return StatHealRecharge;
	}
	return "";
}

replication
{
	// Things the server should send to the client.
	if (true)
		CurrentLevel,CurrentPrestige,CurrentEXP,NextLevelEXP,CurrentSP,LastLevelEXP,bHasNightVision,MinLevelForPrestige,PrestigeSPIncrease,MaxPrestige,bTacticalReload,EnemyHealthRange;
}

simulated final function bool IsWeaponOnPerk(KFWeapon W)
{
	if (class<KFPerk_Survivalist>(BasePerk) != None)
		return true;

	//if (W.AllowedForAllPerks())
	//	return true;
		
	return W!=None && W.GetWeaponPerkClass(BasePerk)==BasePerk;
}

simulated static function string GetPerkIconPath(int Level)
{
	return "img://"$PathName(default.PerkIcon);
}

simulated function PostBeginPlay()
{
	local int i,j;
	local class<Ext_TraitBase> T;

	if (WorldInfo.NetMode==NM_Client)
	{
		PerkStats.Length = 0; // Prevent client desync with client settings.
		PlayerOwner = GetALocalPlayerController();
		SetTimer(0.01,false,'InitPerk');
	}
	else
	{
		RemoteRole = ROLE_None; // Make sure these actors get replicated in order to client.
		PlayerOwner = Controller(Owner);
		if (PlayerOwner==None)
		{
			`Log(Self@"spawned without owner.");
			Destroy();
			return;
		}
		bOwnerNetClient = (PlayerController(Owner)!=None && LocalPlayer(PlayerController(Owner).Player)==None);
		
		// Load trait classes.
		j = 0;
		for (i=0; i<TraitClasses.Length; ++i)
		{
			T = class<Ext_TraitBase>(DynamicLoadObject(TraitClasses[i],Class'Class'));
			if (T==None || !T.Static.IsEnabled(Self))
				continue;
			PerkTraits.Length = j+1;
			PerkTraits[j].TraitType = T;
			++j;
		}
		
		// Setup serverside stat info (for XML log files).
		for (j=0; j<PerkStats.Length; ++j)
		{
			i = DefPerkStats.Find('StatType',PerkStats[j].StatType);
			if (i>=0)
				PerkStats[j].UIName = UIName(DefPerkStats[i]);
			else
			{
				// Fallback to parent perk for trying to find name.
				i = Class'Ext_PerkBase'.Default.DefPerkStats.Find('StatType',PerkStats[j].StatType);
				if (i>=0)
					PerkStats[j].UIName = UIName(Class'Ext_PerkBase'.Default.DefPerkStats[i]);
				else PerkStats[j].UIName = string(PerkStats[j].StatType); // Fallback to stat name then...
			}
		}
	}
}

simulated function InitPerk()
{
	if (PlayerOwner==None)
		PlayerOwner = GetALocalPlayerController();
	if (PerkManager==None)
	{
		foreach DynamicActors(class'ExtPerkManager',PerkManager)
		{
			PerkManager.RegisterPerk(Self);
			break;
		}
	}
}

simulated function Destroyed()
{
	local int i;

	if (PerkManager!=None)
		PerkManager.UnregisterPerk(Self);
	if (WorldInfo.NetMode!=NM_Client)
	{
		for (i=0; i<PerkTraits.Length; ++i)
			PerkTraits[i].TraitType.Static.CleanupTrait(ExtPlayerController(Owner),Self,PerkTraits[i].Data);
	}
}

// For HUD UI
simulated final function string GetLevelString()
{
	return (CurrentPrestige>0 ? (string(CurrentPrestige)$"-"$string(CurrentLevel)) : string(CurrentLevel));
}

// For progress bar on HUD
simulated final function float GetProgressPercent()
{
	return FClamp(float(CurrentEXP-LastLevelEXP) / FMax(float(NextLevelEXP-LastLevelEXP),1.f),0.f,1.f);
}

// Whetever if user can use prestige now.
simulated final function bool CanPrestige()
{
	return (MinLevelForPrestige>=0 && CurrentPrestige<MaxPrestige) ? CurrentLevel>=MinLevelForPrestige : false;
}

// Whetever to save this perk status or not
final function bool HasAnyProgress()
{
	return (CurrentEXP>0 || CurrentPrestige>0);
}

reliable client simulated function ClientReceiveStat(int Index, int MaxValue, int CostPerValue, name Type, int CurValue, float Progress)
{
	local int i;

	if (WorldInfo.NetMode==NM_Client)
	{
		if (PerkStats.Length<=Index)
			PerkStats.Length = Index+1;
		PerkStats[Index].MaxValue = MaxValue;
		PerkStats[Index].CostPerValue = CostPerValue;
		PerkStats[Index].StatType = Type;
		PerkStats[Index].CurrentValue = CurValue;
		PerkStats[Index].DisplayValue = 0.f;
		PerkStats[Index].Progress = Progress;
	}
	i = DefPerkStats.Find('StatType',Type);
	if (i>=0)
		PerkStats[Index].UIName = UIName(DefPerkStats[i]);
	else
	{
		// Fallback to parent perk for trying to find name.
		i = Class'Ext_PerkBase'.Default.DefPerkStats.Find('StatType',Type);
		if (i>=0)
			PerkStats[Index].UIName = UIName(Class'Ext_PerkBase'.Default.DefPerkStats[i]);
		else PerkStats[Index].UIName = string(Type); // Fallback to stat name then...
	}
}

reliable client simulated function ClientSetStatValue(int Index, int NewValue)
{
	if (PerkStats.Length<=Index)
		PerkStats.Length = Index+1;
	PerkStats[Index].CurrentValue = NewValue;
	if (bPerkNetReady)
		ApplyEffects();
}

reliable client simulated function ClientReceiveTrait(int Index, class<Ext_TraitBase> TC, byte Lvl)
{
	if (PerkTraits.Length<=Index)
		PerkTraits.Length = Index+1;
	PerkTraits[Index].TraitType = TC;
	PerkTraits[Index].CurrentLevel = Lvl;
}

reliable client simulated function ClientReceiveTraitData(int Index, string Data)
{
	if (WorldInfo.NetMode==NM_Client)
		PerkTraits[Index].TraitType.Static.ClientSetRepData(Data);
}

reliable client simulated function ClientReceiveTraitLvl(int Index, byte NewLevel)
{
	PerkTraits[Index].CurrentLevel = NewLevel;
}

final function SetPerkStat(name Type, int Value)
{
	local int i;
	
	i = PerkStats.Find('StatType',Type);
	if (i>=0)
		PerkStats[i].CurrentValue = Value;
}

final function int GetPerkStat(name Type)
{
	local int i;
	
	i = PerkStats.Find('StatType',Type);
	if (i==-1)
		return 0;
	return PerkStats[i].CurrentValue;
}

function bool EarnedEXP(int EXP)
{
	local int n;

	bForceNetUpdate = true;
	CurrentEXP+=EXP;
	while (CurrentEXP>=NextLevelEXP && CurrentLevel<MaximumLevel && n<20)
	{
		++n;
		LastLevelEXP = NextLevelEXP;
		NextLevelEXP = GetNeededExp(++CurrentLevel);
	}
	if (n>0)
	{
		CurrentSP+=(n*(StarPointsPerLevel+CurrentPrestige*PrestigeSPIncrease));
		if (PerkManager.PRIOwner!=None && PerkManager.CurrentPerk==Self)
			UpdatePRILevel();
		// TODO - broadcast level up messages.
		if (ExtPlayerController(PlayerOwner)!=None)
			ExtPlayerController(PlayerOwner).ReceiveLevelUp(Self,CurrentLevel);
	}
	return true;
}

final function UpdatePRILevel()
{
	PerkManager.PRIOwner.SetLevelProgress(CurrentLevel,CurrentPrestige,MinimumLevel,MaximumLevel);
}

// XML output
function OutputXML(ExtStatWriter Data)
{
	local int i;

	Data.StartIntendent("perk","class",string(Class.Name));
	Data.WriteValue("perkname",PerkName);
	Data.WriteValue("level",string(CurrentLevel));
	Data.WriteValue("prestige",string(CurrentPrestige));
	Data.WriteValue("exp",string(CurrentEXP));
	Data.WriteValue("points",string(CurrentSP));
	Data.WriteValue("exptilnext",string(NextLevelEXP));
	Data.WriteValue("exponprev",string(LastLevelEXP));
	
	for (i=0; i<PerkStats.Length; ++i)
	{
		if (PerkStats[i].CurrentValue>0)
		{
			Data.StartIntendent("stat","type",string(PerkStats[i].StatType));
			Data.WriteValue("name",GetStatUIStr(i));
			Data.WriteValue("value",string(PerkStats[i].CurrentValue));
			Data.WriteValue("progress",string(PerkStats[i].DisplayValue));
			Data.EndIntendent();
		}
	}
	
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
		{
			Data.StartIntendent("trait","class",string(PerkTraits[i].TraitType.Name));
			Data.WriteValue("name",PerkTraits[i].TraitType.Default.TraitName);
			Data.WriteValue("level",string(PerkTraits[i].CurrentLevel));
			Data.EndIntendent();
		}
	}

	Data.EndIntendent();
}

// Data saving.
function SaveData(ExtSaveDataBase Data)
{
	local int i,j;
	
	// Write current EXP.
	Data.SaveInt(CurrentEXP,3);
	
	// Write current prestige
	Data.SaveInt(CurrentPrestige,3);

	// Count number of given stats
	j = 0;
	for (i=0; i<PerkStats.Length; ++i)
		if (PerkStats[i].CurrentValue>0)
			++j;

	// Then perk stats.
	Data.SaveInt(j);
	for (i=0; i<PerkStats.Length; ++i)
	{
		if (PerkStats[i].CurrentValue>0)
		{
			Data.SaveStr(string(PerkStats[i].StatType));
			Data.SaveInt(PerkStats[i].CurrentValue,1);
		}
	}
	
	// Count bought traits.
	j = 0;
	for (i=0; i<PerkTraits.Length; ++i)
		if (PerkTraits[i].CurrentLevel>0)
			++j;

	// Then traits.
	Data.SaveInt(j);
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
		{
			Data.SaveStr(string(PerkTraits[i].TraitType));
			Data.SaveInt(PerkTraits[i].CurrentLevel);
		}
	}
}

// Data loading.
function LoadData(ExtSaveDataBase Data)
{
	local int i,j,l,n;
	local string S;

	CurrentEXP = Data.ReadInt(3);
	// if (MinimalDataLevel > 0)
	// {
	// 	i = GetNeededExp(MinimalDataLevel-1)
	// 	if (i > CurrentEXP)
	// 		CurrentEXP = i
	// }
	
	if (Data.GetArVer()>=1)
		CurrentPrestige = Data.ReadInt(3);

	l = Data.ReadInt(); // Perk stats length.
	for (i=0; i<l; ++i)
	{
		S = Data.ReadStr();
		n = Data.ReadInt(1);
		for (j=0; j<PerkStats.Length; ++j)
			if (S~=string(PerkStats[j].StatType))
			{
				PerkStats[j].CurrentValue = n;
				break;
			}
	}
	
	l = Data.ReadInt(); // Traits stats length.
	for (i=0; i<l; ++i)
	{
		S = Data.ReadStr();
		n = Data.ReadInt();
		for (j=0; j<PerkTraits.Length; ++j)
			if (S~=string(PerkTraits[j].TraitType))
			{
				PerkTraits[j].CurrentLevel = n;
				break;
			}
	}
}

final function int CalcLevelForExp(int InExp)
{
	local int i,a,b;

	// Fast method to calc level for a player.
	b = MaximumLevel+1;
	a = Min(MinimumLevel,b);
	while (true)
	{
		if (a==b || (a+1)==b)
		{
			if (a<MaximumLevel && InExp>=GetNeededExp(a))
				++a;
			break;
		}
		i = a+((b-a)>>1);
		if (InExp<GetNeededExp(i)) // Lower!
			b = i;
		else a = i; // Higher!
	}
	return Clamp(a,MinimumLevel,MaximumLevel);
}

// Initialize perk after stats have been loaded.
function SetInitialLevel()
{
	local int i,a,b;
	local byte MT,j;

	if (MinimalDataLevel > 0)
	{
		i = GetNeededExp(MinimalDataLevel-1);
		if (i > CurrentEXP)
			CurrentEXP = i;
	}

	// Set to initial level player is on after configures has loaded.
	CurrentLevel = CalcLevelForExp(CurrentEXP);
	CurrentSP = CurrentLevel*(StarPointsPerLevel+CurrentPrestige*PrestigeSPIncrease);
	NextLevelEXP = GetNeededExp(CurrentLevel);
	LastLevelEXP = (CurrentLevel>MinimumLevel ? GetNeededExp(CurrentLevel-1) : 0);
	
	// Now verify the points player used on individual stats.
	for (i=0; i<PerkStats.Length; ++i)
	{
		if (PerkStats[i].CurrentValue>0)
		{
			PerkStats[i].CurrentValue = Min(PerkStats[i].CurrentValue,PerkStats[i].MaxValue);
			a = PerkStats[i].CurrentValue*PerkStats[i].CostPerValue;
			if (CurrentSP>a)
				CurrentSP-=a;
			else if (CurrentSP<=0) // No points at all for this.
				PerkStats[i].CurrentValue = 0;
			else // Nope, reduce the stat!
			{
				a = CurrentSP/PerkStats[i].CostPerValue;
				PerkStats[i].CurrentValue = a;
				CurrentSP-=(a*PerkStats[i].CostPerValue);
			}
		}
	}

	// Then verify trait levels and costs.
	MT = 0;
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
		{
			PerkTraits[i].CurrentLevel = Min(PerkTraits[i].CurrentLevel,PerkTraits[i].TraitType.Default.NumLevels);
			
			if (PerkTraits[i].TraitType.Default.LoadPriority>0)
				MT = Max(MT,PerkTraits[i].TraitType.Default.LoadPriority);
			else
			{
				if (!PerkTraits[i].TraitType.Static.MeetsRequirements(PerkTraits[i].CurrentLevel-1,Self))
					a = 0;
				else
				{
					for (a=0; a<PerkTraits[i].CurrentLevel; ++a)
					{
						b = PerkTraits[i].TraitType.Static.GetTraitCost(a);
						if (b>CurrentSP)
							break;
						CurrentSP-=b;
					}
				}
				PerkTraits[i].CurrentLevel = a;
				if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].Data==None)
					PerkTraits[i].Data = PerkTraits[i].TraitType.Static.Initializefor (Self,ExtPlayerController(Owner));
			}
		}
		if (PerkTraits[i].CurrentLevel==0 && PerkTraits[i].Data!=None)
			PerkTraits[i].TraitType.Static.CleanupTrait(ExtPlayerController(Owner),Self,PerkTraits[i].Data);
	}
	
	// Delayed loads.
	for (j=1; j<=MT; ++j)
	{
		for (i=0; i<PerkTraits.Length; ++i)
		{
			if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].TraitType.Default.LoadPriority==j)
			{
				if (!PerkTraits[i].TraitType.Static.MeetsRequirements(PerkTraits[i].CurrentLevel-1,Self))
					a = 0;
				else
				{
					for (a=0; a<PerkTraits[i].CurrentLevel; ++a)
					{
						b = PerkTraits[i].TraitType.Static.GetTraitCost(a);
						if (b>CurrentSP)
							break;
						CurrentSP-=b;
					}
				}
				PerkTraits[i].CurrentLevel = a;
				if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].Data==None)
					PerkTraits[i].Data = PerkTraits[i].TraitType.Static.Initializefor (Self,ExtPlayerController(Owner));
			}
			if (PerkTraits[i].CurrentLevel==0 && PerkTraits[i].Data!=None)
				PerkTraits[i].TraitType.Static.CleanupTrait(ExtPlayerController(Owner),Self,PerkTraits[i].Data);
		}
	}

	ApplyEffects();
	if (PerkManager.CurrentPerk==Self)
		ActivateTraits();
}

// Check the needed amount of EXP for a perk.
function int GetNeededExp(int LevelNum)
{
	if (LevelNum<MinimumLevel || LevelNum>=MaximumLevel)
		return 0;
	LevelNum-=MinimumLevel;
	LevelNum = (FirstLevelExp+(LevelNum*LevelUpExpCost)+(LevelNum*LevelNum*LevelUpIncCost));
	if (CurrentPrestige>0 && PrestigeXPReduce>0)
		LevelNum *= (1.f / (1.f + PrestigeXPReduce*CurrentPrestige));
	return LevelNum;
}

// Configure initialization.
static function CheckConfig()
{
	local int i;
	local class<Ext_TraitBase> T;

	if (Default.ConfigVersion!=Default.CurrentConfigVer)
	{
		UpdateConfigs(Default.ConfigVersion);
		Default.ConfigVersion = Default.CurrentConfigVer;
		StaticSaveConfig();
	}
	for (i=0; i<Default.TraitClasses.Length; ++i)
	{
		T = class<Ext_TraitBase>(DynamicLoadObject(Default.TraitClasses[i],Class'Class'));
		if (T!=None)
			T.Static.CheckConfig();
	}
}

static function UpdateConfigs(int OldVer)
{
	local int i,j;

	if (OldVer==0)
	{
		Default.FirstLevelExp = 400;
		Default.LevelUpExpCost = 500;
		Default.LevelUpIncCost = 65;
		Default.MinimumLevel = 0;
		Default.MaximumLevel = 150;
		Default.StarPointsPerLevel = 15;
		
		// Prestige.
		Default.MinLevelForPrestige = 140;
		Default.PrestigeSPIncrease = 1;
		Default.MaxPrestige = 20;
		Default.MinimalDataLevel = 0;
		Default.PrestigeXPReduce = 0.05;
		
		Default.PerkStats.Length = 0;
		AddStatsCfg(0);
		Default.TraitClasses.Length = Default.DefTraitList.Length;
		for (i=0; i<Default.DefTraitList.Length; ++i)
			Default.TraitClasses[i] = PathName(Default.DefTraitList[i]);
	}
	else
	{
		// Add progress.
		if (OldVer==1)
		{
			for (i=0; i<Default.PerkStats.Length; ++i)
			{
				j = Default.DefPerkStats.Find('StatType',Default.PerkStats[i].StatType);
				if (j>=0)
					Default.PerkStats[i].Progress = Default.DefPerkStats[j].Progress;
			}
			// Add off-perk damage stat.
			AddStatsCfg(12);
		}
		else if (OldVer<=3)
			AddStatsCfg(13); // Add self damage.
		else if (OldVer<=4)
			AddStatsCfg(15); // Add poison damage.
		else if (OldVer<=7)
			AddStatsCfg(16); // Add sonic/fire damage.
		else if (OldVer<=12)
			AddStatsCfg(18); // Add all damage.
		else if (OldVer<=13)
			AddStatsCfg(19); // Add HeadDamage and HealRecharge
		if (OldVer<=5)
		{
			// Add prestige
			Default.MinLevelForPrestige = 140;
			Default.PrestigeSPIncrease = 1;
			Default.MaxPrestige = 20;
			Default.PrestigeXPReduce = 0.05;
		}

		Default.TraitClasses.Length = Default.DefTraitList.Length;
		for (i=0; i<Default.DefTraitList.Length; ++i)
			Default.TraitClasses[i] = PathName(Default.DefTraitList[i]);
	}
}

static final function AddStatsCfg(int StartRange)
{
	local int i,j;

	j = Default.PerkStats.Length;
	for (i=StartRange; i<Default.DefPerkStats.Length; ++i)
	{
		if (Default.DefPerkStats[i].bHiddenConfig || Default.PerkStats.Find('StatType',Default.DefPerkStats[i].StatType)>=0) // Don't add if already found for some reason.
			continue;
		Default.PerkStats.Length = j+1;
		Default.PerkStats[j].MaxValue = Default.DefPerkStats[i].MaxValue;
		Default.PerkStats[j].CostPerValue = Default.DefPerkStats[i].CostPerValue;
		Default.PerkStats[j].StatType = Default.DefPerkStats[i].StatType;
		Default.PerkStats[j].Progress = Default.DefPerkStats[i].Progress;
		++j;
	}
}

// WebAdmin UI stuff.
static function InitWebAdmin(ExtWebAdmin_UI UI)
{
	local class<Ext_TraitBase> T;
	local int i;

	UI.AddSettingsPage("Perk "$Default.PerkName,Default.Class,Default.WebConfigs,GetValue,ApplyValue);
	
	for (i=0; i<Default.TraitClasses.Length; ++i)
	{
		T = class<Ext_TraitBase>(DynamicLoadObject(Default.TraitClasses[i],Class'Class'));
		if (T==None || UI.HasConfigfor (T))
			continue;
		T.Static.InitWebAdmin(UI);
	}
}

static function string GetValue(name PropName, int ElementIndex)
{
	switch (PropName)
	{
	case 'FirstLevelExp':
		return string(Default.FirstLevelExp);
	case 'LevelUpExpCost':
		return string(Default.LevelUpExpCost);
	case 'LevelUpIncCost':
		return string(Default.LevelUpIncCost);
	case 'MinimumLevel':
		return string(Default.MinimumLevel);
	case 'MaximumLevel':
		return string(Default.MaximumLevel);
	case 'StarPointsPerLevel':
		return string(Default.StarPointsPerLevel);
	case 'TraitClasses':
		return ElementIndex==-1 ? string(Default.TraitClasses.Length) : Default.TraitClasses[ElementIndex];
	case 'PerkStats':
		return ElementIndex==-1 ? string(Default.PerkStats.Length) : Default.PerkStats[ElementIndex].StatType$","$Default.PerkStats[ElementIndex].MaxValue$","$Default.PerkStats[ElementIndex].CostPerValue$","$Default.PerkStats[ElementIndex].Progress;
	case 'MinLevelForPrestige':
		return string(Default.MinLevelForPrestige);
	case 'PrestigeSPIncrease':
		return string(Default.PrestigeSPIncrease);
	case 'MinimalDataLevel':
		return string(Default.MinimalDataLevel);
	case 'MaxPrestige':
		return string(Default.MaxPrestige);
	case 'PrestigeXPReduce':
		return string(Default.PrestigeXPReduce);
	}
}

static function ApplyValue(name PropName, int ElementIndex, string Value)
{
	switch (PropName)
	{
	case 'FirstLevelExp':
		Default.FirstLevelExp = int(Value);		break;
	case 'LevelUpExpCost':
		Default.LevelUpExpCost = int(Value);	break;
	case 'MinimalDataLevel':
		Default.MinimalDataLevel = int(Value);	break;
	case 'LevelUpIncCost':
		Default.LevelUpIncCost = int(Value);	break;
	case 'MinimumLevel':
		Default.MinimumLevel = int(Value);		break;
	case 'MaximumLevel':
		Default.MaximumLevel = int(Value);		break;
	case 'StarPointsPerLevel':
		Default.StarPointsPerLevel = int(Value); break;
	case 'TraitClasses':
		if (Value=="#DELETE")
			Default.TraitClasses.Remove(ElementIndex,1);
		else
		{
			if (ElementIndex>=Default.TraitClasses.Length)
				Default.TraitClasses.Length = ElementIndex+1;
			Default.TraitClasses[ElementIndex] = Value;
		}
		break;
	case 'PerkStats':
		if (Value=="#DELETE")
			Default.PerkStats.Remove(ElementIndex,1);
		else
		{
			if (ElementIndex>=Default.PerkStats.Length)
				Default.PerkStats.Length = ElementIndex+1;
			Default.PerkStats[ElementIndex] = ParsePerkStatStr(Value);
		}
		break;
	case 'MinLevelForPrestige':
		Default.MinLevelForPrestige = int(Value); break;
	case 'PrestigeSPIncrease':
		Default.PrestigeSPIncrease = int(Value); break;
	case 'MaxPrestige':
		Default.MaxPrestige = int(Value);		break;
	case 'PrestigeXPReduce':
		Default.PrestigeXPReduce = float(Value); break;
	default:
		return;
	}
	StaticSaveConfig();
}

static final function FPerkStat ParsePerkStatStr(string S)
{
	local FPerkStat Res;
	local int i;

	i = InStr(S,",");
	if (i==-1)
		return Res;
	Res.StatType = name(Left(S,i));
	S = Mid(S,i+1);
	i = InStr(S,",");
	if (i==-1)
		return Res;
	Res.MaxValue = int(Left(S,i));
	S = Mid(S,i+1);
	i = InStr(S,",");
	if (i==-1)
		return Res;
	Res.CostPerValue = int(Left(S,i));
	Res.Progress = float(Mid(S,i+1));
	return Res;
}

// Amount and iStat values are verified already by ServerExtMut.
function bool IncrementStat(int iStat, int Amount)
{
	PerkStats[iStat].CurrentValue+=Amount;
	if (bOwnerNetClient)
		ClientSetStatValue(iStat,PerkStats[iStat].CurrentValue);
	PerkManager.bStatsDirty = true;
	ApplyEffects();
	bForceNetUpdate = true;
	return true;
}

simulated function ApplyEffects()
{
	local int i;
	
	for (i=0; i<PerkStats.Length; ++i)
	{
		if (PerkStats[i].CurrentValue!=PerkStats[i].OldValue)
		{
			PerkStats[i].OldValue = PerkStats[i].CurrentValue;
			PerkStats[i].DisplayValue = ApplyEffect(PerkStats[i].StatType,PerkStats[i].CurrentValue,PerkStats[i].Progress/100.f);
		}
	}
}

// Notify that player just spawned.
function ApplyEffectsTo(KFPawn_Human P)
{
	local int i;
	local bool bSec;
	
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
		{
			if (PerkTraits[i].TraitType.Default.bPostApplyEffect)
				bSec = true;
			else PerkTraits[i].TraitType.Static.ApplyEffectOn(P,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
		}
	}
	if (bSec)
	{
		for (i=0; i<PerkTraits.Length; ++i)
		{
			if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].TraitType.Default.bPostApplyEffect)
				PerkTraits[i].TraitType.Static.ApplyEffectOn(P,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
		}
	}
}

// Player joined/perk changed.
function ActivateTraits()
{
	local int i;
	local KFPawn_Human KFP;
	local bool bSec;
	
	KFP = KFPawn_Human(PlayerOwner.Pawn);
	if (KFP!=None && !KFP.IsAliveAndWell())
		KFP = None;

	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
		{
			PerkTraits[i].TraitType.Static.TraitActivate(Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
			if (KFP!=None)
			{
				if (PerkTraits[i].TraitType.Default.bPostApplyEffect)
					bSec = true;
				else PerkTraits[i].TraitType.Static.ApplyEffectOn(KFP,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
			}
		}
	}
	if (bSec)
	{
		for (i=0; i<PerkTraits.Length; ++i)
		{
			if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].TraitType.Default.bPostApplyEffect)
				PerkTraits[i].TraitType.Static.ApplyEffectOn(KFP,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
		}
	}
}

// Player disconnected/perk changed.
function DeactivateTraits()
{
	local int i;
	
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
			PerkTraits[i].TraitType.Static.TraitDeActivate(Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
	}
}

simulated unreliable client function ClientAuth()
{
	if (Owner==None)
		SetOwner(PlayerOwner);
	ServerAck();
}

unreliable server function ServerAck()
{
	if (!bClientAuthorized)
	{
		bClientAuthorized = true;
		RepState = 0;
		RepIndex = 0;
		SetTimer(0.01+FRand()*0.025,true,'ReplicateTimer');
	}
}

function ReplicateTimer()
{
	switch (RepState)
	{
	case 0: // Send all perk stats
		if (RepIndex>=PerkStats.Length)
		{
			++RepState;
			RepIndex = 0;
		}
		else
		{
			ClientReceiveStat(RepIndex,PerkStats[RepIndex].MaxValue,PerkStats[RepIndex].CostPerValue,PerkStats[RepIndex].StatType,PerkStats[RepIndex].CurrentValue,PerkStats[RepIndex].Progress);
			++RepIndex;
		}
		break;
	case 1: // Send all traits
		if (RepIndex>=PerkTraits.Length)
			++RepState;
		else
		{
			ClientReceiveTrait(RepIndex,PerkTraits[RepIndex].TraitType,PerkTraits[RepIndex].CurrentLevel);
			ClientReceiveTraitData(RepIndex,PerkTraits[RepIndex].TraitType.Static.GetRepData());
			++RepIndex;
		}
		break;
	default:
		ClearTimer('ReplicateTimer');
		bPerkNetReady = true;
		ClientIsReady(); // Notify client were ready.
	}
}

simulated reliable client function ClientIsReady()
{
	bPerkNetReady = true;
	ApplyEffects();
}

simulated function string GetStatUIStr(int iStat)
{
	local string S;
	local bool bLoop;

	S = string(Abs(PerkStats[iStat].DisplayValue*100.f));
	bLoop = true;

	// Chop off float digits that aren't needed.
	while (bLoop)
	{
		switch (Right(S,1))
		{
		case "0":
			S = Left(S,Len(S)-1);
			break;
		case ".":
			S = Left(S,Len(S)-1);
			bLoop = false;
			break;
		default:
			bLoop = false;
		}
	}
	return Repl(PerkStats[iStat].UIName,"&",S);
}

final function UnloadStats(optional byte Mode)
{
	local int i,j;
	local KFPawn_Human KFP;

	PerkManager.bStatsDirty = true;
	if (Mode<=1)
	{
		// Reset stats.
		for (i=0; i<PerkStats.Length; ++i)
		{
			if (PerkStats[i].CurrentValue>0)
			{
				CurrentSP+=(PerkStats[i].CurrentValue*PerkStats[i].CostPerValue);
				PerkStats[i].CurrentValue = 0;
				if (bOwnerNetClient)
					ClientSetStatValue(i,0);
			}
		}
		ApplyEffects();
	}
	if (Mode==0 || Mode==2)
	{
		KFP = KFPawn_Human(PlayerOwner.Pawn);
		if (KFP!=None && !KFP.IsAliveAndWell())
			KFP = None;

		// Reset traits.
		for (i=0; i<PerkTraits.Length; ++i)
		{
			if (PerkTraits[i].CurrentLevel>0)
			{
				for (j=0; j<PerkTraits[i].CurrentLevel; ++j)
					CurrentSP+=PerkTraits[i].TraitType.Static.GetTraitCost(j);
				if (PerkManager.CurrentPerk==Self)
				{
					PerkTraits[i].TraitType.Static.TraitDeActivate(Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
					if (KFP!=None)
						PerkTraits[i].TraitType.Static.CancelEffectOn(KFP,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
				}
				PerkTraits[i].TraitType.Static.CleanupTrait(ExtPlayerController(Owner),Self,PerkTraits[i].Data);
				PerkTraits[i].CurrentLevel = 0;
				if (bOwnerNetClient)
					ClientReceiveTraitLvl(i,0);
			}
		}
	}
}

function FullReset(optional bool bNotPrestige)
{
	UnloadStats();

	// Set minimum values.
	CurrentEXP = 0;
	if (!bNotPrestige)
		CurrentPrestige = 0;
	CurrentLevel = MinimumLevel;
	CurrentSP = CurrentLevel*(StarPointsPerLevel+CurrentPrestige*PrestigeSPIncrease);
	NextLevelEXP = GetNeededExp(CurrentLevel);
	LastLevelEXP = 0;
	
	if (PerkManager.CurrentPerk==Self && PerkManager.PRIOwner!=None)
	{
		PerkManager.PRIOwner.SetLevelProgress(CurrentLevel,CurrentPrestige,MinimumLevel,MaximumLevel);
		PerkManager.PRIOwner.bForceNetUpdate = true;
	}

	bForceNetUpdate = true;
}

function bool PreventDeath(KFPawn_Human Player, Controller Killer, Class<DamageType> DamType)
{
	local int i;
	
	// Doing 2 passes of this so that things don't go out of order (spawn retaliation effect when you get redeemed etc)
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0 && PerkTraits[i].TraitType.Default.bHighPriorityDeath && PerkTraits[i].TraitType.Static.PreventDeath(Player,Killer,DamType,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data))
			return true;
	}
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0 && !PerkTraits[i].TraitType.Default.bHighPriorityDeath && PerkTraits[i].TraitType.Static.PreventDeath(Player,Killer,DamType,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data))
			return true;
	}
	return false;
}

simulated function PlayerDied()
{
	local int i;

	if (WorldInfo.NetMode!=NM_Client)
	{
		for (i=0; i<PerkTraits.Length; ++i)
		{
			if (PerkTraits[i].CurrentLevel>0)
				PerkTraits[i].TraitType.Static.PlayerDied(Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
		}
	}
}

// Stat modifier functions.
simulated function float ApplyEffect(name Type, float Value, float Progress)
{
	local bool bActivePerk;
	
	bActivePerk = (PerkManager!=None && PerkManager.CurrentPerk==Self);
	switch (Type)
	{
	case 'Speed':
		Modifiers[0] = 1.f + (Value*Progress);
		break;
	case 'Damage':
		Modifiers[1] = 1.f + (Value*Progress);
		break;
	case 'Recoil':
		Modifiers[2] = 1.f / (1.f+Value*Progress);
		break;
	case 'Spread':
		Modifiers[3] = 1.f / (1.f+Value*Progress);
		break;
	case 'Rate':
		Modifiers[4] = 1.f / (1.f+Value*Progress);
		break;
	case 'Reload':
		Modifiers[5] = 1.f / (1.f+Value*Progress);
		break;
	case 'Health':
		Modifiers[6] = 1.f + (Value*Progress);
		if (bActivePerk && PlayerOwner.Pawn!=None)
		{
			PlayerOwner.Pawn.HealthMax = PlayerOwner.Pawn.Default.Health;
			ModifyHealth(PlayerOwner.Pawn.HealthMax);
		}
		break;
	case 'KnockDown':
		Modifiers[7] = FMin(1.f + (Value*Progress),2.f);
		return (Modifiers[7]-1.f);
	case 'Welder':
		Modifiers[8] = 1.f + (Value*Progress);
		break;
	case 'Heal':
		Modifiers[9] = 1.f + (Value*Progress);
		break;
	case 'Mag':
		Modifiers[10] = 1.f + (Value*Progress);
		if (bActivePerk && WorldInfo.NetMode!=NM_Client && PlayerOwner.Pawn!=None && PlayerOwner.Pawn.InvManager!=None)
			UpdateAmmoStatus(PlayerOwner.Pawn.InvManager);
		break;
	case 'Spare':
		Modifiers[11] = 1.f + (Value*Progress);
		if (bActivePerk && WorldInfo.NetMode!=NM_Client && PlayerOwner.Pawn!=None && PlayerOwner.Pawn.InvManager!=None)
			UpdateAmmoStatus(PlayerOwner.Pawn.InvManager);
		break;
	case 'OffDamage':
		Modifiers[12] = 1.f + (Value*Progress);
		break;
	case 'SelfDamage':
		Modifiers[13] = 1.f / (1.f+Value*Progress);
		break;
	case 'Armor':
		Modifiers[14] = (Value*Progress*100.f);
		if (bActivePerk && KFPawn_Human(PlayerOwner.Pawn)!=None)
		{
			KFPawn_Human(PlayerOwner.Pawn).MaxArmor = KFPawn_Human(PlayerOwner.Pawn).Default.MaxArmor;
			ModifyArmor(KFPawn_Human(PlayerOwner.Pawn).MaxArmor);
		}
		return FMin(Value*Progress,1.55);
	case 'PoisonDmg':
		Modifiers[15] = 1.f / (1.f+Value*Progress);
		break;
	case 'SonicDmg':
		Modifiers[16] = 1.f / (1.f+Value*Progress);
		break;
	case 'FireDmg':
		Modifiers[17] = 1.f / (1.f+Value*Progress);
		break;
	case 'AllDmg':
		Modifiers[18] = 1.f / (1.f+Value*Progress);
		break;
	case 'HeadDamage':
		Modifiers[19] = Value*Progress;
		break;
	case 'HealRecharge':
		Modifiers[20] = 1.f / (1.f+Value*Progress);
		break;
	}
	return (Value*Progress);
}

simulated function ModifyDamageGiven(out int InDamage, optional Actor DamageCauser, optional KFPawn_Monster MyKFPM, optional KFPlayerController DamageInstigator, optional class<KFDamageType> DamageType, optional int HitZoneIdx)
{
	if (BasePerk==None || (DamageType!=None && DamageType.Default.ModifierPerkList.Find(BasePerk)>=0) || (KFWeapon(DamageCauser)!=None && IsWeaponOnPerk(KFWeapon(DamageCauser))))
	{
		if (HitZoneIdx == 0)
			InDamage *= (Modifiers[1] + Modifiers[19]);
		else
			InDamage *= Modifiers[1];
	}
	else if (DamageType==None || DamageType.Name!='KFDT_SuicideExplosive')
		InDamage *= Modifiers[12];
}

simulated function ModifyDamageTaken(out int InDamage, optional class<DamageType> DamageType, optional Controller InstigatedBy)
{
	if (InDamage>0)
	{
		if ((InstigatedBy==None || InstigatedBy==PlayerOwner) && class<KFDamageType>(DamageType)!=None)
			InDamage *= Modifiers[13];
		else if (Modifiers[15]<1 && class<KFDT_Toxic>(DamageType)!=None)
			InDamage = Max(InDamage*Modifiers[15],1); // Do at least 1 damage.
		else if (Modifiers[16]<1 && class<KFDT_Sonic>(DamageType)!=None)
			InDamage = Max(InDamage*Modifiers[16],1);
		else if (Modifiers[17]<1 && class<KFDT_Fire>(DamageType)!=None)
			InDamage = Max(InDamage*Modifiers[17],1);
		if (Modifiers[18]<1 && InstigatedBy!=None && InstigatedBy!=PlayerOwner)
			InDamage = Max(InDamage*Modifiers[18],1);
	}
}

simulated function ModifyRecoil(out float CurrentRecoilModifier, KFWeapon KFW)
{
	if (IsWeaponOnPerk(KFW))
		CurrentRecoilModifier *= Modifiers[2];
}

simulated function ModifySpread(out float InSpread)
{
	InSpread *= Modifiers[3];
}

simulated function ModifyRateOfFire(out float InRate, KFWeapon KFW)
{
	if (IsWeaponOnPerk(KFW))
		InRate *= Modifiers[4];
}

simulated function float GetReloadRateScale(KFWeapon KFW)
{
	return (IsWeaponOnPerk(KFW) ? Modifiers[5] : 1.f);
}

function ModifyHealth(out int InHealth)
{
	InHealth *= Modifiers[6];
}

function ModifyArmor(out byte MaxArmor)
{
	MaxArmor = Min(MaxArmor+Modifiers[14],255);
}

function float GetKnockdownPowerModifier()
{
	return Modifiers[7];
}

function float GetStunPowerModifier(optional class<DamageType> DamageType, optional byte HitZoneIdx)
{
	return Modifiers[7];
}

simulated function ModifyMeleeAttackSpeed(out float InDuration);

function AddDefaultInventory(KFPawn P)
{
	local int i;

	if (PrimaryWeapon!=None)
		P.DefaultInventory.AddItem(PrimaryWeapon);
	P.DefaultInventory.AddItem(PrimaryMelee);
	if (KFInventoryManager(P.InvManager)!=None)
		KFInventoryManager(P.InvManager).MaxCarryBlocks = KFInventoryManager(P.InvManager).Default.MaxCarryBlocks+Modifiers[10];
	
	for (i=0; i<PerkTraits.Length; ++i)
	{
		if (PerkTraits[i].CurrentLevel>0)
			PerkTraits[i].TraitType.Static.AddDefaultInventory(P,Self,PerkTraits[i].CurrentLevel,PerkTraits[i].Data);
	}
}

simulated function ModifyWeldingRate(out float FastenRate, out float UnfastenRate)
{
	FastenRate *= Modifiers[8];
	UnfastenRate *= Modifiers[8];
}

function bool RepairArmor(Pawn HealTarget)
{
	return false;
}

function bool ModifyHealAmount(out float HealAmount)
{
	HealAmount*=Modifiers[9];
	return false;
}

simulated function ModifyMagSizeAndNumber(KFWeapon KFW, out int MagazineCapacity, optional array< Class<KFPerk> > WeaponPerkClass, optional bool bSecondary=false, optional name WeaponClassname)
{
	if (MagazineCapacity>2 && (KFW==None ? WeaponPerkClass.Find(BasePerk)>=0 : IsWeaponOnPerk(KFW))) // Skip boomstick for this.
		MagazineCapacity = Min(MagazineCapacity*Modifiers[10],255);
}

simulated function ModifySpareAmmoAmount(KFWeapon KFW, out int PrimarySpareAmmo, optional const out STraderItem TraderItem, optional bool bSecondary)
{
	if (KFW==None ? TraderItem.AssociatedPerkClasses.Find(BasePerk)>=0 : IsWeaponOnPerk(KFW))
		PrimarySpareAmmo*=Modifiers[11];
}

simulated function bool ShouldMagSizeModifySpareAmmo(KFWeapon KFW, optional Class<KFPerk> WeaponPerkClass)
{
	return (KFW==None ? WeaponPerkClass==BasePerk : IsWeaponOnPerk(KFW));
}

final function UpdateAmmoStatus(InventoryManager Inv)
{
	local KFWeapon W;

	foreach Inv.InventoryActors(class'KFWeapon',W)
	{
		if (IsWeaponOnPerk(W))
			W.ReInitializeAmmoCounts(PerkManager);
	}
}

simulated function ModifyHealerRechargeTime(out float RechargeRate)
{
	RechargeRate *= Modifiers[20];
}

simulated function DrawSpecialPerkHUD(Canvas C)
{
	if (EnemyHealthRange>0 && PlayerOwner!=None && KFPawn_Human(PlayerOwner.Pawn)!=None)
		DrawEnemyHealth(C);
}

simulated final function DrawEnemyHealth(Canvas C)
{
	local KFPawn_Monster KFPM;
	local vector X,CameraLocation;
	
	X = vector(PlayerOwner.Pawn.GetViewRotation());
	CameraLocation = PlayerOwner.Pawn.GetPawnViewLocation();

	foreach WorldInfo.AllPawns(class'KFPawn_Monster',KFPM,CameraLocation,EnemyDistDraw[EnemyHealthRange-1])
	{
		if (KFPM.IsAliveAndWell() && `TimeSince(KFPM.Mesh.LastRenderTime)<0.1f && KFPM.CanShowHealth() && KFPM.GetTeamNum()!=0 && (X Dot (KFPM.Location - CameraLocation))>0)
			DrawZedHealthbar(C,KFPM,CameraLocation);
	}
}

simulated final function DrawZedHealthbar(Canvas C, KFPawn_Monster KFPM, vector CameraLocation)
{
	local vector ScreenPos, TargetLocation;
	local float HealthBarLength, HealthbarHeight, HealthScale;

	HealthbarLength = FMin(50.f * (float(C.SizeX) / 1024.f), 50.f) * class'KFGFxHudWrapper'.Default.FriendlyHudScale;
	HealthbarHeight = FMin(6.f * (float(C.SizeX) / 1024.f), 6.f) * class'KFGFxHudWrapper'.Default.FriendlyHudScale;
	HealthScale = FClamp(float(KFPM.Health) / float(KFPM.HealthMax),0.f,1.f);

	if (KFPM.bCrawler && KFPM.Floor.Z <=  -0.7f && KFPM.Physics == PHYS_Spider)
	{
		TargetLocation = KFPM.Location + vect(0,0,-1) * KFPM.GetCollisionHeight() * 1.2;
	}
	else
	{
		TargetLocation = KFPM.Location + vect(0,0,1) * KFPM.GetCollisionHeight() * 1.2;
	}

	ScreenPos = C.Project(TargetLocation);
	if (ScreenPos.X < 0 || ScreenPos.X > C.SizeX || ScreenPos.Y < 0 || ScreenPos.Y > C.SizeY || !FastTrace(TargetLocation,  CameraLocation))
		return;

	C.EnableStencilTest(true);
	C.SetDrawColor(0, 0, 0, 255);
	C.SetPos(ScreenPos.X - HealthBarLength * 0.5, ScreenPos.Y);
	C.DrawTileStretched(class'KFPerk_Commando'.Default.WhiteMaterial, HealthbarLength, HealthbarHeight, 0, 0, 32, 32);

	C.SetDrawColor(237, 8, 0, 255);
	C.SetPos(ScreenPos.X - HealthBarLength * 0.5 + 1.0, ScreenPos.Y + 1.0);
	C.DrawTileStretched(class'KFPerk_Commando'.Default.WhiteMaterial, (HealthBarLength - 2.0) * HealthScale, HealthbarHeight - 2.0, 0, 0, 32, 32);
	C.EnableStencilTest(false);
}

function PlayerKilled(KFPawn_Monster Victim, class<DamageType> DamageType);

function ModifyBloatBileDoT(out float DoTScaler)
{
	DoTScaler = Modifiers[15];
}

simulated function bool GetIsUberAmmoActive(KFWeapon KFW)
{
	return false;
}

function UpdatePerkHeadShots(ImpactInfo Impact, class<DamageType> DamageType, int NumHit);

function CheckForAirborneAgent(KFPawn HealTarget, class<DamageType> DamType, int HealAmount);

simulated function float GetZedTimeModifier(KFWeapon W)
{
	return 0.f;
}

simulated function bool GetUsingTactialReload(KFWeapon KFW)
{
	return (bTacticalReload && IsWeaponOnPerk(KFW));
}

simulated function float GetIronSightSpeedModifier(KFWeapon KFW)
{
	return 1.f;
}

function OnWaveEnded();
function NotifyZedTimeStarted();

simulated function float GetZedTimeExtensions(byte Level)
{
	return 1.f;
}

defaultproperties
{
	CurrentConfigVer=14
	bOnlyRelevantToOwner=true
	bCanBeGrabbed=true
	NetUpdateFrequency=1
	GrenadeClass=class'KFProj_FragGrenade'
	PerkGrenade=class'KFProj_FragGrenade'
	SuperGrenade=class'ExtProj_SUPERGrenade'
	HealExpUpNum=12
	WeldExpUpNum=180
	ToxicDartDamage=15
	NetPriority=4
	
	SecondaryWeaponDef=class'KFWeapDef_9mm'
	KnifeWeaponDef=class'KFWeapDef_Knife_Commando'
	GrenadeWeaponDef=class'KFWeapDef_Grenade_Support'
	
	DefTraitList.Add(class'Ext_TraitGrenadeUpg')
	DefTraitList.Add(class'Ext_TraitNightvision')
	DefTraitList.Add(class'Ext_TraitAmmoReg')
	DefTraitList.Add(class'Ext_TraitHealthReg')
	DefTraitList.Add(class'Ext_TraitArmorReg')
	DefTraitList.Add(class'Ext_TraitCarryCap')
	DefTraitList.Add(class'Ext_TraitGrenadeCap')
	DefTraitList.Add(class'Ext_TraitMedicPistol')
	DefTraitList.Add(class'Ext_TraitZED_Summon')
	DefTraitList.Add(class'Ext_TraitZED_Health')
	DefTraitList.Add(class'Ext_TraitZED_Damage')
	DefTraitList.Add(class'Ext_TraitZED_SummonExt')
	DefTraitList.Add(class'Ext_TraitGhost')
	DefTraitList.Add(class'Ext_TraitRetali')
	DefTraitList.Add(class'Ext_TraitDuracell')
	DefTraitList.Add(class'Ext_TraitRagdoll')
	DefTraitList.Add(class'Ext_TraitAutoFire')
	DefTraitList.Add(class'Ext_TraitBunnyHop')
	DefTraitList.Add(class'Ext_TraitKnockback')

	WebConfigs.Add((PropType=0,PropName="FirstLevelExp",UIName="First Level XP",UIDesc="EXP required for the FIRST level"))
	WebConfigs.Add((PropType=0,PropName="LevelUpExpCost",UIName="Level Up XP",UIDesc="EXP cost for every next level (Level * ThisValue"))
	WebConfigs.Add((PropType=0,PropName="LevelUpIncCost",UIName="Level Up Inc XP",UIDesc="Increased EXP cost for every level ((Level^2) * ThisValue)"))
	WebConfigs.Add((PropType=0,PropName="MinimumLevel",UIName="Minimum Level",UIDesc="The minimum level of players"))
	WebConfigs.Add((PropType=0,PropName="MaximumLevel",UIName="Maximum Level",UIDesc="The maximum level of players"))
	WebConfigs.Add((PropType=0,PropName="StarPointsPerLevel",UIName="Star Points Per Lvl",UIDesc="Number of star points players earn per level"))
	WebConfigs.Add((PropType=2,PropName="TraitClasses",UIName="Trait Classes",UIDesc="The class names of traits players can buy",NumElements=-1))
	WebConfigs.Add((PropType=2,PropName="PerkStats",UIName="Perk Stats",UIDesc="List of perk stats (format in: StatName,Max Stat,Cost Per Stat,Progress Per Level)",NumElements=-1))
	WebConfigs.Add((PropType=0,PropName="MinLevelForPrestige",UIName="Min Level For Prestige",UIDesc="Minimum level required to prestige the perk (-1 = disabled)"))
	WebConfigs.Add((PropType=0,PropName="PrestigeSPIncrease",UIName="Prestige SP Increase",UIDesc="Star points increase per level for every prestige"))
	WebConfigs.Add((PropType=0,PropName="MaxPrestige",UIName="Max Prestige",UIDesc="Maximum prestige level"))
	WebConfigs.Add((PropType=0,PropName="PrestigeXPReduce",UIName="Prestige XP Reduce",UIDesc="Percent amount of XP cost is reduced for each prestige (1.0 = 1/2, or 50 % of XP)"))
	// WebConfigs.Add((PropType=0,PropName="MinimalDataLevel",UIName="Minimal Real Level",UIDesc="Minimal level for new players or who loads from saves"))
	
	// TODO: localize
	DefPerkStats(0)=(MaxValue=50,CostPerValue=1,StatType="Speed",Progress=0.4)
	DefPerkStats(1)=(MaxValue=1000,CostPerValue=1,StatType="Damage",Progress=0.5)
	DefPerkStats(2)=(MaxValue=90,CostPerValue=1,StatType="Recoil",Progress=1)
	DefPerkStats(3)=(MaxValue=80,CostPerValue=1,StatType="Spread",Progress=0.75)
	DefPerkStats(4)=(MaxValue=1000,CostPerValue=1,StatType="Rate",Progress=0.5)
	DefPerkStats(5)=(MaxValue=1000,CostPerValue=1,StatType="Reload",Progress=0.5)
	DefPerkStats(6)=(MaxValue=150,CostPerValue=1,StatType="Health",Progress=1)
	DefPerkStats(7)=(MaxValue=100,CostPerValue=1,StatType="KnockDown",Progress=1)
	DefPerkStats(8)=(MaxValue=200,CostPerValue=1,StatType="Welder",bHiddenConfig=true,Progress=0.5)
	DefPerkStats(9)=(MaxValue=400,CostPerValue=1,StatType="Heal",bHiddenConfig=true,Progress=0.5)
	DefPerkStats(10)=(MaxValue=400,CostPerValue=1,StatType="Mag",Progress=1)
	DefPerkStats(11)=(MaxValue=500,CostPerValue=1,StatType="Spare",Progress=1)
	DefPerkStats(12)=(MaxValue=1000,CostPerValue=1,StatType="OffDamage",Progress=0.25)
	DefPerkStats(13)=(MaxValue=1000,CostPerValue=1,StatType="SelfDamage",Progress=1,bHiddenConfig=true)
	DefPerkStats(14)=(MaxValue=150,CostPerValue=1,StatType="Armor",Progress=1)
	DefPerkStats(15)=(MaxValue=1000,CostPerValue=1,StatType="PoisonDmg",Progress=1.5,bHiddenConfig=true)
	DefPerkStats(16)=(MaxValue=1000,CostPerValue=1,StatType="SonicDmg",Progress=1.5,bHiddenConfig=true)
	DefPerkStats(17)=(MaxValue=1000,CostPerValue=1,StatType="FireDmg",Progress=1.5,bHiddenConfig=true)
	DefPerkStats(18)=(MaxValue=500,CostPerValue=1,StatType="AllDmg",Progress=0.25)
	DefPerkStats(19)=(MaxValue=500,CostPerValue=1,StatType="HeadDamage",Progress=1,bHiddenConfig=true)
	DefPerkStats(20)=(MaxValue=200,CostPerValue=1,StatType="HealRecharge",Progress=0.5,bHiddenConfig=true)

	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(0.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(1.f)
	Modifiers.Add(0.f)
	Modifiers.Add(1.f)
	
	EnemyDistDraw.Add(500)
	EnemyDistDraw.Add(700)
	EnemyDistDraw.Add(1000)
	EnemyDistDraw.Add(1600)
}