/*
	Extended Perk Manager
	Written by Marco
*/
Class ExtPerkManager extends KFPerk;

enum EReplicateState
{
	REP_CustomCharacters,
	REP_CustomInventory,
	REP_PerkClasses,
	REP_Done
};
const CUR_SaveVersion=2;

var int UserDataVersion;
var transient float LastNapalmTime;

// Server -> Client rep status
var byte RepState;
var int RepIndex;

var array<Ext_PerkBase> UserPerks;
var Ext_PerkBase CurrentPerk;
var int ExpUpStatus[2];
var string StrPerkName;

var ExtPlayerReplicationInfo PRIOwner;
var Controller PlayerOwner;

// Stats
var int TotalEXP,TotalKills,TotalPlayTime;

var bool bStatsDirty,bServerReady,bUserStatsBroken,bCurrentlyHealing;

// SWAT Enforcer
var array<Actor> CurrentBumpedActors; // The unique list of actors that have been bumped before the last cooldown reset
var float LastBumpTime;               // The last time a zed was bumped using battering ram
var float BumpCooldown;               // The amount of time between when the last actor was bumped and another actor can be bumped again
var float BumpMomentum;               // Amount of momentum when bumping zeds
var int BumpDamageAmount;             // Amount of damage Battering Ram bumps deal
var class<DamageType> BumpDamageType; // Damage type used for Battering Ram bump damage

replication
{
	// Things the server should send to the client.
	if (bNetDirty)
		CurrentPerk;
}

final function SetGrenadeCap(byte AddedCap)
{
	MaxGrenadeCount = Default.MaxGrenadeCount + AddedCap;
	if (RepState==REP_Done)
		ClientSetGrenadeCap(MaxGrenadeCount);
}

simulated reliable client function ClientSetGrenadeCap(byte NewCap)
{
	MaxGrenadeCount = NewCap;
}

function bool ApplyPerkClass(class<Ext_PerkBase> P)
{
	local int i;
	
	for (i=0; i<UserPerks.Length; ++i)
		if (UserPerks[i].Class==P)
		{
			ApplyPerk(UserPerks[i]);
			return true;
		}
	return false;
}

function bool ApplyPerkName(string S)
{
	local int i;
	
	for (i=0; i<UserPerks.Length; ++i)
		if (string(UserPerks[i].Class.Name)~=S)
		{
			ApplyPerk(UserPerks[i]);
			return true;
		}
	return false;
}

function ApplyPerk(Ext_PerkBase P)
{
	local KFPawn_Human HP;
	local KFInventoryManager InvMan;
	local Ext_T_ZEDHelper H;
	local int i;
	
	if (P==None)
		return;
		
	if (PlayerOwner.Pawn != None)
	{
		InvMan = KFInventoryManager(PlayerOwner.Pawn.InvManager);
		if (InvMan != None)
			InvMan.MaxCarryBlocks = InvMan.Default.MaxCarryBlocks;
			
		foreach PlayerOwner.Pawn.ChildActors(class'Ext_T_ZEDHelper',H)
		{
			H.Destroy();
		}
		
		HP = KFPawn_Human(PlayerOwner.Pawn);
		if (HP != None)
			HP.DefaultInventory = HP.Default.DefaultInventory;
	}
	
	if (CurrentPerk != None)
	{
		CurrentPerk.DeactivateTraits();
		
		for (i=0; i<CurrentPerk.PerkTraits.Length; ++i)
		{
			CurrentPerk.PerkTraits[i].TraitType.Static.CancelEffectOn(KFPawn_Human(PlayerOwner.Pawn),CurrentPerk,CurrentPerk.PerkTraits[i].CurrentLevel,CurrentPerk.PerkTraits[i].Data);
		}
	}
		
	bStatsDirty = true;
	CurrentPerk = P;
	
	if (PRIOwner!=None)
	{
		PRIOwner.ECurrentPerk = P.Class;
		PRIOwner.FCurrentPerk = P;
		P.UpdatePRILevel();
	}
	
	if (CurrentPerk!=None)
	{
		CurrentPerk.ActivateTraits();
		
		if (PlayerOwner.Pawn != None)
		{
			HP = KFPawn_Human(PlayerOwner.Pawn);
			if (HP != None)
			{
				HP.HealthMax = HP.default.Health;
				HP.MaxArmor = HP.default.MaxArmor;
			
				ModifyHealth(HP.HealthMax);
				ModifyArmor(HP.MaxArmor);
				
				if (HP.Health > HP.HealthMax) HP.Health = HP.HealthMax;
				if (HP.Armor > HP.MaxArmor) HP.Armor = HP.MaxArmor;
			}
		}
	}
}

simulated final function Ext_PerkBase FindPerk(class<Ext_PerkBase> P)
{
	local int i;
	
	for (i=0; i<UserPerks.Length; ++i)
		if (UserPerks[i].Class==P)
			return UserPerks[i];
	return None;
}

simulated function PostBeginPlay()
{
	SetTimer(0.01,false,'InitPerks');
	if (WorldInfo.NetMode!=NM_Client)
		SetTimer(1,true,'CheckPlayTime');
}

simulated function InitPerks()
{
	local Ext_PerkBase P;
	
	if (WorldInfo.NetMode==NM_Client)
	{
		foreach DynamicActors(class'Ext_PerkBase',P)
			if (P.PerkManager!=Self)
				RegisterPerk(P);
	}
	else if (PRIOwner!=PlayerOwner.PlayerReplicationInfo) // See if was assigned an inactive PRI.
	{
		PRIOwner = ExtPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);
		if (PRIOwner!=None)
		{
			if (CurrentPerk!=None)
			{
				PRIOwner.ECurrentPerk = CurrentPerk.Class;
				CurrentPerk.UpdatePRILevel();
			}
			PRIOwner.RepKills = TotalKills;
			PRIOwner.RepEXP = TotalEXP;
			PRIOwner.SetInitPlayTime(TotalPlayTime);
			PRIOwner.PerkManager = Self;
		}
	}
}

function CheckPlayTime()
{
	++TotalPlayTime; // Stats.
}

function ServerInitPerks()
{
	local int i;
	
	for (i=0; i<UserPerks.Length; ++i)
		UserPerks[i].SetInitialLevel();
	bServerReady = true;
	CurrentPerk = None;
	if (StrPerkName!="")
		ApplyPerkName(StrPerkName);
	if (CurrentPerk==None)
		ApplyPerk(UserPerks[Rand(UserPerks.Length)]);
}

simulated function RegisterPerk(Ext_PerkBase P)
{
	UserPerks[UserPerks.Length] = P;
	P.PerkManager = Self;
}

simulated function UnregisterPerk(Ext_PerkBase P)
{
	UserPerks.RemoveItem(P);
	P.PerkManager = None;
}

function Destroyed()
{
	local int i;
	
	for (i=(UserPerks.Length-1); i>=0; --i)
	{
		UserPerks[i].PerkManager = None;
		UserPerks[i].Destroy();
	}
}

function EarnedEXP(int EXP, optional byte Mode)
{
	// `log("EarnedEXP" @ GetScriptTrace());
	if (CurrentPerk!=None)
	{
		// Limit how much EXP we got for healing and welding.
		switch (Mode)
		{
		case 1:
			ExpUpStatus[0]+=EXP;
			EXP = ExpUpStatus[0]/CurrentPerk.WeldExpUpNum;
			if (EXP>0)
				ExpUpStatus[0]-=(EXP*CurrentPerk.WeldExpUpNum);
			break;
		case 2:
			ExpUpStatus[1]+=EXP;
			EXP = ExpUpStatus[1]/CurrentPerk.HealExpUpNum;
			if (EXP>0)
				ExpUpStatus[1]-=(EXP*CurrentPerk.HealExpUpNum);
			break;
		}
		if (EXP>0 && CurrentPerk.EarnedEXP(EXP))
		{
			TotalEXP+=EXP;
			PRIOwner.RepEXP+=EXP;
			bStatsDirty = true;
		}
	}
}

// XML stat writing
function OutputXML(ExtStatWriter Data)
{
	local string S;
	local int i;

	Data.StartIntendent("user","ver",string(CUR_SaveVersion));
	Data.WriteValue("id64",OnlineSubsystemSteamworks(class'GameEngine'.Static.GetOnlineSubsystem()).UniqueNetIdToInt64(PRIOwner.UniqueId));
	Data.WriteValue("name",PRIOwner.PlayerName);
	Data.WriteValue("exp",string(TotalEXP));
	Data.WriteValue("kills",string(TotalKills));
	Data.WriteValue("time",string(TotalPlayTime));
	if (ExtPlayerController(Owner)!=None && ExtPlayerController(Owner).PendingPerkClass!=None)
		S = string(ExtPlayerController(Owner).PendingPerkClass.Name);
	else S = (CurrentPerk!=None ? string(CurrentPerk.Class.Name) : "None");
	Data.WriteValue("activeperk",S);

	for (i=0; i<UserPerks.Length; ++i)
		if (UserPerks[i].HasAnyProgress())
			UserPerks[i].OutputXML(Data);

	Data.EndIntendent();
}

// Data saving.
function SaveData(ExtSaveDataBase Data)
{
	local int i,o;

	Data.FlushData();
	Data.SetSaveVersion(++UserDataVersion);
	Data.SetArVer(CUR_SaveVersion);

	// Write global stats.
	Data.SaveInt(TotalEXP,3);
	Data.SaveInt(TotalKills,3);
	Data.SaveInt(TotalPlayTime,3);
	
	// Write character.
	if (PRIOwner!=None)
		PRIOwner.SaveCustomCharacter(Data);
	else class'ExtPlayerReplicationInfo'.Static.DummySaveChar(Data);

	// Write selected perk.
	if (ExtPlayerController(Owner)!=None && ExtPlayerController(Owner).PendingPerkClass!=None)
		Data.SaveStr(string(ExtPlayerController(Owner).PendingPerkClass.Name));
	else Data.SaveStr(CurrentPerk!=None ? string(CurrentPerk.Class.Name) : "");

	// Count how many progressed perks we have.
	o = 0;
	for (i=0; i<UserPerks.Length; ++i)
		if (UserPerks[i].HasAnyProgress())
			++o;
	
	// Then write count we have.
	Data.SaveInt(o);
	
	// Then perk stats.
	for (i=0; i<UserPerks.Length; ++i)
	{
		if (!UserPerks[i].HasAnyProgress()) // Skip this perk.
			continue;

		Data.SaveStr(string(UserPerks[i].Class.Name));
		o = Data.TellOffset(); // Mark checkpoint.
		Data.SaveInt(0,1); // Reserve space for later.
		UserPerks[i].SaveData(Data);

		// Now save the skip offset for perk data incase perk gets removed from server.
		Data.SeekOffset(o);
		Data.SaveInt(Data.TotalSize(),1);
		Data.ToEnd();
	}
}

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

	Data.ToStart();
	UserDataVersion = Data.GetSaveVersion();

	// Read global stats.
	TotalEXP = Data.ReadInt(3);
	TotalKills = Data.ReadInt(3);
	TotalPlayTime = Data.ReadInt(3);

	// Read character.
	if (PRIOwner!=None)
	{
		PRIOwner.RepKills = TotalKills;
		PRIOwner.RepEXP = TotalEXP;
		PRIOwner.SetInitPlayTime(TotalPlayTime);
		PRIOwner.LoadCustomCharacter(Data);
	}
	else class'ExtPlayerReplicationInfo'.Static.DummyLoadChar(Data);

	// Find selected perk.
	CurrentPerk = None;
	StrPerkName = Data.ReadStr();

	l = Data.ReadInt(); // Perk stats length.
	for (i=0; i<l; ++i)
	{
		S = Data.ReadStr();
		o = Data.ReadInt(1); // Read skip offset.
		Data.PushEOFLimit(o);
		for (j=0; j<UserPerks.Length; ++j)
			if (S~=string(UserPerks[j].Class.Name))
			{
				UserPerks[j].LoadData(Data);
				break;
			}
		Data.PopEOFLimit();
		Data.SeekOffset(o); // Jump to end of this section.
	}
	bStatsDirty = false;
}

function AddDefaultInventory(KFPawn P)
{
	local KFInventoryManager KFIM;

	if (P != none && P.InvManager != none)
	{
		KFIM = KFInventoryManager(P.InvManager);
		if (KFIM != none)
		{
			//Grenades added on spawn
			KFIM.GiveInitialGrenadeCount();
		}

		if (CurrentPerk!=None)
			CurrentPerk.AddDefaultInventory(P);
	}
}

simulated function PlayerDied()
{
	if (CurrentPerk!=None)
		CurrentPerk.PlayerDied();
}

function PreNotifyPlayerLeave()
{
	if (CurrentPerk!=None)
		CurrentPerk.DeactivateTraits();
}

// Start client replication of perks data.
// Call this once the stats has been properly loaded serverside!
function InitiateClientRep()
{
	RepState = 0;
	RepIndex = 0;
	SetTimer(0.01,true,'ReplicateTimer');
}

function ReplicateTimer()
{
	switch (RepState)
	{
	case REP_CustomCharacters: // Replicate custom characters.
		if (RepIndex>=PRIOwner.CustomCharList.Length)
		{
			PRIOwner.AllCharReceived();
			RepIndex = 0;
			++RepState;
		}
		else
		{
			PRIOwner.ReceivedCharacter(RepIndex,PRIOwner.CustomCharList[RepIndex]);
			++RepIndex;
		}
		break;
	case REP_CustomInventory: // Replicate custom trader inventory
		if (!PRIOwner.OnRepNextItem(PRIOwner,RepIndex))
		{
			RepIndex = 0;
			++RepState;
		}
		else ++RepIndex;
		break;
	case REP_PerkClasses: // Open up all actor channel connections.
		if (RepIndex>=UserPerks.Length)
		{
			RepIndex = 0;
			++RepState;
		}
		else if (UserPerks[RepIndex].bClientAuthorized)
		{
			if (UserPerks[RepIndex].bPerkNetReady)
				++RepIndex;
		}
		else
		{
			UserPerks[RepIndex].RemoteRole = ROLE_SimulatedProxy;
			if (UserPerks[RepIndex].NextAuthTime<WorldInfo.RealTimeSeconds)
			{
				UserPerks[RepIndex].NextAuthTime = WorldInfo.RealTimeSeconds+0.5;
				UserPerks[RepIndex].ClientAuth();
			}
		}
		break;
	default:
		if (MaxGrenadeCount!=Default.MaxGrenadeCount)
			ClientSetGrenadeCap(MaxGrenadeCount);
		ClearTimer('ReplicateTimer');
	}
}

function bool CanEarnSmallRadiusKillXP(class<DamageType> DT)
{
	return true;
}

simulated function ModifySpeed(out float Speed)
{
	if (CurrentPerk!=None)
		Speed *= CurrentPerk.Modifiers[0];
}

function ModifyDamageGiven(out int InDamage, optional Actor DamageCauser, optional KFPawn_Monster MyKFPM, optional KFPlayerController DamageInstigator, optional class<KFDamageType> DamageType, optional int HitZoneIdx)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyDamageGiven(InDamage,DamageCauser,MyKFPM,DamageInstigator,DamageType,HitZoneIdx);
}

simulated function ModifyDamageTaken(out int InDamage, optional class<DamageType> DamageType, optional Controller InstigatedBy)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyDamageTaken(InDamage,DamageType,InstigatedBy);
}

simulated function ModifyRecoil(out float CurrentRecoilModifier, KFWeapon KFW)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyRecoil(CurrentRecoilModifier,KFW);
}

simulated function ModifySpread(out float InSpread)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifySpread(InSpread);
}

simulated function ModifyRateOfFire(out float InRate, KFWeapon KFW)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyRateOfFire(InRate,KFW);
}

simulated function float GetReloadRateScale(KFWeapon KFW)
{
	return (CurrentPerk!=None ? CurrentPerk.GetReloadRateScale(KFW) : 1.f);
}

simulated function bool GetUsingTactialReload(KFWeapon KFW)
{
	return (CurrentPerk!=None ? CurrentPerk.GetUsingTactialReload(KFW) : false);
}

function ModifyHealth(out int InHealth)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyHealth(InHealth);
}

function ModifyArmor(out byte MaxArmor)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyArmor(MaxArmor);
}

function float GetKnockdownPowerModifier(optional class<DamageType> DamageType, optional byte BodyPart, optional bool bIsSprinting=false)
{
	return (CurrentPerk!=None ? CurrentPerk.GetKnockdownPowerModifier() : 1.f);
}

function float GetStumblePowerModifier(optional KFPawn KFP, optional class<KFDamageType> DamageType, optional out float CooldownModifier, optional byte BodyPart)
{
	return (CurrentPerk!=None ? CurrentPerk.GetKnockdownPowerModifier() : 1.f);
}

function float GetStunPowerModifier(optional class<DamageType> DamageType, optional byte HitZoneIdx)
{
	return (CurrentPerk!=None ? CurrentPerk.GetStunPowerModifier(DamageType,HitZoneIdx) : 1.f);
}

simulated function ModifyMeleeAttackSpeed(out float InDuration, KFWeapon KFW)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyMeleeAttackSpeed(InDuration);
}

simulated function class<KFProj_Grenade> GetGrenadeClass()
{
	return (CurrentPerk!=None ? CurrentPerk.GrenadeClass : GrenadeClass);
}

simulated function ModifyWeldingRate(out float FastenRate, out float UnfastenRate)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyWeldingRate(FastenRate,UnfastenRate);
}

simulated function bool HasNightVision()
{
	return (CurrentPerk!=None ? CurrentPerk.bHasNightVision : false);
}

function bool RepairArmor(Pawn HealTarget)
{
	return (CurrentPerk!=None ? CurrentPerk.RepairArmor(HealTarget) : false);
}

function bool ModifyHealAmount(out float HealAmount)
{
	return (CurrentPerk!=None ? CurrentPerk.ModifyHealAmount(HealAmount) : false);
}

function bool CanNotBeGrabbed()
{
	return (CurrentPerk!=None ? !CurrentPerk.bCanBeGrabbed : false);
}

simulated function ModifyMagSizeAndNumber(KFWeapon KFW, out int MagazineCapacity, optional array< Class<KFPerk> > WeaponPerkClass, optional bool bSecondary=false, optional name WeaponClassname)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyMagSizeAndNumber(KFW,MagazineCapacity,WeaponPerkClass,bSecondary,WeaponClassname);
}

simulated function ModifySpareAmmoAmount(KFWeapon KFW, out int PrimarySpareAmmo, optional const out STraderItem TraderItem, optional bool bSecondary=false)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifySpareAmmoAmount(KFW,PrimarySpareAmmo,TraderItem,bSecondary);
}

simulated function ModifyMaxSpareAmmoAmount(KFWeapon KFW, out int SpareAmmoCapacity, optional const out STraderItem TraderItem, optional bool bSecondary=false)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifySpareAmmoAmount(KFW,SpareAmmoCapacity,TraderItem,bSecondary);
}

simulated function bool ShouldMagSizeModifySpareAmmo(KFWeapon KFW, optional Class<KFPerk> WeaponPerkClass)
{
	return (CurrentPerk!=None ? CurrentPerk.ShouldMagSizeModifySpareAmmo(KFW,WeaponPerkClass) : false);
}

simulated function ModifyHealerRechargeTime(out float RechargeRate)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyHealerRechargeTime(RechargeRate);
}

simulated function bool CanExplosiveWeld()
{
	return (CurrentPerk!=None ? CurrentPerk.bExplosiveWeld : false);
}

simulated function bool IsOnContactActive()
{
	return (CurrentPerk!=None ? CurrentPerk.bExplodeOnContact : false);
}

function bool CanSpreadNapalm()
{
	if (CurrentPerk!=None && CurrentPerk.bNapalmFire && LastNapalmTime!=WorldInfo.TimeSeconds)
	{
		LastNapalmTime = WorldInfo.TimeSeconds; // Avoid infinite script recursion in KFPawn_Monster.
		return true;
	}
	return false;
}

simulated function bool IsRangeActive()
{
	return MyPRI!=None ? MyPRI.bExtraFireRange : false;
}

simulated function DrawSpecialPerkHUD(Canvas C)
{
	if (CurrentPerk!=None)
		CurrentPerk.DrawSpecialPerkHUD(C);
}

function PlayerKilled(KFPawn_Monster Victim, class<DamageType> DamageType)
{
	if (CurrentPerk!=None)
		CurrentPerk.PlayerKilled(Victim,DamageType);
}

function ModifyBloatBileDoT(out float DoTScaler)
{
	if (CurrentPerk!=None)
		CurrentPerk.ModifyBloatBileDoT(DoTScaler);
}

simulated function bool GetIsUberAmmoActive(KFWeapon KFW)
{
	return (CurrentPerk!=None ? CurrentPerk.GetIsUberAmmoActive(KFW) : false);
}

function UpdatePerkHeadShots(ImpactInfo Impact, class<DamageType> DamageType, int NumHit)
{
	if (CurrentPerk!=None)
		CurrentPerk.UpdatePerkHeadShots(Impact,DamageType,NumHit);
}

function CheckForAirborneAgent(KFPawn HealTarget, class<DamageType> DamType, int HealAmount)
{
	if (!bCurrentlyHealing && CurrentPerk!=None)
	{
		// Using boolean to avoid infinite recursion.
		bCurrentlyHealing = true;
		CurrentPerk.CheckForAirborneAgent(HealTarget,DamType,HealAmount);
		bCurrentlyHealing = false;
	}
}

simulated function float GetZedTimeModifier(KFWeapon W)
{
	return (CurrentPerk!=None ? CurrentPerk.GetZedTimeModifier(W) : 0.f);
}

// Poison darts
function bool IsAcidicCompoundActive()
{
	return (CurrentPerk!=None ? CurrentPerk.bToxicDart : false);
}

function ModifyACDamage(out int InDamage)
{
	if (CurrentPerk!=None && CurrentPerk.bToxicDart)
		InDamage += CurrentPerk.ToxicDartDamage;
}

// Zombie explosion!
function bool CouldBeZedShrapnel(class<KFDamageType> KFDT)
{
	return (CurrentPerk!=None ? (CurrentPerk.bFireExplode && class<KFDT_Fire>(KFDT)!=None) : false);
}

simulated function bool ShouldShrapnel()
{
	return (CurrentPerk!=None ? (CurrentPerk.bFireExplode && Rand(3)==0) : false);
}

function GameExplosion GetExplosionTemplate()
{
	return class'KFPerk_Firebug'.Default.ExplosionTemplate;
}

// Additional functions
function OnWaveEnded()
{
	CurrentPerk.OnWaveEnded();
}

function NotifyZedTimeStarted()
{
	CurrentPerk.NotifyZedTimeStarted();
}

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

// SWAT:
simulated function bool HasHeavyArmor()
{
	return (CurrentPerk!=None && CurrentPerk.bHeavyArmor);
}

simulated function float GetIronSightSpeedModifier(KFWeapon KFW)
{
	return (CurrentPerk!=None ? CurrentPerk.GetIronSightSpeedModifier(KFW) : 1.f);
}

simulated function float GetCrouchSpeedModifier(KFWeapon KFW)
{
	return (CurrentPerk!=None ? CurrentPerk.GetIronSightSpeedModifier(KFW) : 1.f);
}

simulated function bool ShouldKnockDownOnBump()
{
	return (CurrentPerk!=None && CurrentPerk.bHasSWATEnforcer);
}

simulated function OnBump(Actor BumpedActor, KFPawn_Human BumpInstigator, vector BumpedVelocity, rotator BumpedRotation)
{
	local KFPawn_Monster KFPM;
	local bool CanBump;

	if (ShouldKnockDownOnBump() && Normal(BumpedVelocity) dot Vector(BumpedRotation) > 0.7f)
	{
		KFPM = KFPawn_Monster(BumpedActor);
		if (KFPM != none)
		{
			// cooldown so that the same zed can't be bumped multiple frames back to back
			//	especially relevant if they can't be knocked down or stumbled so the player is always bumping them
			if (WorldInfo.TimeSeconds - LastBumpTime > BumpCooldown)
			{
				CurrentBumpedActors.length = 0;
				CurrentBumpedActors.AddItem(BumpedActor);
				CanBump = true;
			}
			// if still within the cooldown time, can still bump the actor as long as it hasn't been bumped yet
			else if (CurrentBumpedActors.Find(BumpedActor) == INDEX_NONE)
			{
				CurrentBumpedActors.AddItem(BumpedActor);
				CanBump = true;
			}

			LastBumpTime = WorldInfo.TimeSeconds;

			if (CanBump)
			{
				if (KFPM.IsHeadless())
				{
					KFPM.TakeDamage(KFPM.HealthMax, BumpInstigator.Controller, BumpInstigator.Location,
						Normal(vector(BumpedRotation)) * BumpMomentum, BumpDamageType);
				}
				else
				{
					KFPM.TakeDamage(BumpDamageAmount, BumpInstigator.Controller, BumpInstigator.Location,
						Normal(vector(BumpedRotation)) * BumpMomentum, BumpDamageType);
					KFPM.Knockdown(BumpedVelocity * 3, vect(1, 1, 1), KFPM.Location, 1000, 100);
				}
			}
		}
	}
}

// DEMO:
simulated function bool ShouldRandSirenResist()
{
	return (Ext_PerkDemolition(CurrentPerk)!=None ? Ext_PerkDemolition(CurrentPerk).bSirenResistance : false);
}

simulated function bool IsAoEActive()
{
	return (Ext_PerkDemolition(CurrentPerk)!=None ? Ext_PerkDemolition(CurrentPerk).AOEMult > 1.0f : false);
}

simulated function bool ShouldSacrifice()
{
	return (Ext_PerkDemolition(CurrentPerk)!=None ? (Ext_PerkDemolition(CurrentPerk).bCanUseSacrifice && !Ext_PerkDemolition(CurrentPerk).bUsedSacrifice) : false);
}

simulated function bool ShouldNeverDud()
{
	return (Ext_PerkDemolition(CurrentPerk)!=None ? Ext_PerkDemolition(CurrentPerk).bProfessionalActive : false);
}

function NotifyPerkSacrificeExploded()
{
	if (Ext_PerkDemolition(CurrentPerk) != none) Ext_PerkDemolition(CurrentPerk).bUsedSacrifice = true;
}

simulated function float GetAoERadiusModifier()
{
	return (Ext_PerkDemolition(CurrentPerk)!=None ? Ext_PerkDemolition(CurrentPerk).GetAoERadiusModifier() : 1.0);
}

// MEDIC:
simulated function bool GetHealingSpeedBoostActive()
{
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).GetHealingSpeedBoostActive() : false);
}

simulated function bool GetHealingDamageBoostActive()
{
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).GetHealingDamageBoostActive() : false);
}

simulated function bool GetHealingShieldActive()
{
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).GetHealingShieldActive() : false);
}

simulated function float GetSelfHealingSurgePct()
{ 
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).GetSelfHealingSurgePct() : 0.f);
}

function bool IsToxicDmgActive()
{
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).bUseToxicDamage : false);
}

static function class<KFDamageType> GetToxicDmgTypeClass()
{
	return class'Ext_PerkFieldMedic'.static.GetToxicDmgTypeClass();
}

static function ModifyToxicDmg(out int ToxicDamage)
{
	ToxicDamage = class'Ext_PerkFieldMedic'.static.ModifyToxicDmg(ToxicDamage);
}

simulated function float GetSnarePower(optional class<DamageType> DamageType, optional byte HitZoneIdx)
{
	return (Ext_PerkFieldMedic(CurrentPerk)!=None ? Ext_PerkFieldMedic(CurrentPerk).GetSnarePower(DamageType, HitZoneIdx) : 0.f);
}

// SUPPORT:
simulated function bool CanRepairDoors()
{
	return (Ext_PerkSupport(CurrentPerk)!=None ? Ext_PerkSupport(CurrentPerk).CanRepairDoors() : false);
}

simulated function float GetPenetrationModifier(byte Level, class<KFDamageType> DamageType, optional bool bForce )
{
	return (Ext_PerkSupport(CurrentPerk)!=None ? Ext_PerkSupport(CurrentPerk).GetPenetrationModifier(Level, DamageType, bForce) : 0.f);
}

// Other
function ApplySkillsToPawn()
{
	if (CheckOwnerPawn())
	{
		OwnerPawn.UpdateGroundSpeed();
		OwnerPawn.bMovesFastInZedTime = false;

		if (MyPRI == none)
			MyPRI = KFPlayerReplicationInfo(OwnerPawn.PlayerReplicationInfo);

		ApplyWeightLimits();
	}
}

defaultproperties
{
	bTickIsDisabled=false
	NetPriority=3.5
	
	// SWAT bumping
	BumpCooldown = 0.1f
	BumpMomentum=1.f
	BumpDamageAmount=450
	BumpDamageType=class'KFDT_SWATBatteringRam'
}