// Additional player info used in achievements
class ScrnPlayerInfo extends Info
    config(ScrnBalance);

// variable below are initially set by ScrnGameRules
var ScrnGameRules GameRules;
var PlayerController PlayerOwner;
var ScrnPlayerInfo NextPlayerInfo;
var byte StartWave; // wave number, when player joined the game 0 - first wave
// -----


var LinkedReplicationInfo CustomReplicationInfo;    // for use by mod authors to link PRI.CustomReplicationInfo

const IGNORE_STAT = 0x7FFFFFFF;

// used to backup ServerPerks stats
struct TCustomStat {
    var class<SRCustomProgress> CustomStatClass;
    var int Progress;
};

struct TPerkStats {
    var bool bSet;

    var int RDamageHealedStat,
        RWeldingPointsStat, RShotgunDamageStat,
        RHeadshotKillsStat,
        RStalkerKillsStat, RBullpupDamageStat,
        RMeleeDamageStat,
        RFlameThrowerDamageStat,
        RExplosivesDamageStat;
    var array<TCustomStat> CustomStats;
};
var TPerkStats GameStartStats; // perk stats at the game start or when player just joined the game
var globalconfig array<string> ExcludeBonusStats;


struct TWeapInfo {
    var KFWeapon Weapon;
    var class<KFWeapon> WeaponClass;
    var class<KFWeaponDamageType> DamType;
    var byte PickupWave; // wave number, when weapon was picked up
    var    PlayerController PrevOwner; // player who owned this weapon before us
    var bool bPrevOwnerDead; // was PrevOwner dead when we picked up his weapon?

    var float LastDmgTime, LastKillTime; // time when last damage was made

    var int TotalKills, TotalHeadshots, TotalDecaps, TotalDamage;

    var bool bHeadshot; // Was last damage made from this weapon a headshot?
    var int RowHeadshots; //number of headshots in a row made from this weapon
    var int HeadshotsPerShot, HeadshotsPerMagazine, HeadshotsPerWave;

    var int KillsPerShot; //number of kills made from this weapon without releasing the trigger or firing again
    var int KillsPerMagazine; //number of kills made from this weapon without reloading
    var int KillsPerWave; //number of kills made from this weapon in wave

    var int DecapsPerShot, DecapsPerMagazine, DecapsPerWave; // decapitations
    var int DamagePerShot, DamagePerMagazine, DamagePerWave; // damages

    // Minimal values to trigger an event. If Value >= Trigger value, event will be called.
    var int TriggerRowHeadshots,
        TriggerHeadshotsPerShot, TriggerHeadshotsPerMagazine,
        TriggerKillsPerShot, TriggerKillsPerMagazine,
        TriggerDecapsPerShot, TriggerDecapsPerMagazine,
        TriggerDamagePerShot, TriggerDamagePerMagazine;
};
var array<TWeapInfo> WeapInfos;

var bool bDied;  // did player died during the way. Valid also during the next trader time.
var byte Deaths; // how many times player died in the game (excluding suicides at trader time)
var byte DeathsByMonster; // how many times player died in the game (excluding suicides at trader time)
var Controller LastKilledBy; // enemy who killed player last time

var float LastDmgTime, LastKillTime;
var float LastDamage; // last damage value
var class<KFWeaponDamageType> LastDamageType;
var KFMonster LastDamagedMonster; // last monster, who was damaged by the player
var bool bHeadshot; // was last damage made by this player a headshot?
var byte RowHeadshots; //number of headshots in a row made by this player
var int HeadshotsPerWave, HeadshotsPerGame;
var int BodyshotsPerWave, BodyshotsPerGame; // this number doesn't include damages with bCheckForHeadShots=false
var int KillsPerWave; // total kills per game are already tracked in PRI
var int DecapsPerWave, DecapsPerGame; // number of decapitations in the current wave
var int DamagePerWave, DamagePerGame;

var int DamageReceivedPerWave, DamageReceivedPerGame; // damage received from monsters
var int HealedPointsInWave;
var int MedicDamage;
var int MEDICXP_PER_1000DMG;

// Minimal values to trigger an event. If Value >= Trigger value, event will be called.
var int TriggerRowHeadshots;

var KFWeapon LastFiredWeapon;
var int LastWeapInfoIndex; // index in WeapInfos array of last updated record

// how much dosh player donated to teammates and how much he received back.
// CashFound - cash spawned on the map (wasn't dropped by a player)
var int CashDonated, CashReceived, CashFound;
var int CashDonatedPerWave, CashReceivedPerWave, CashFoundPerWave;

//stuctures to store custom data form achievement handlers
struct TCustomData {
    var ScrnAchHandlerBase AchHandler;
    var name StatName;
    var bool bWaveReset; // if true, stat will be automatically reset at the new wave begin
    var String StrVal;
    var int IntVal;
    var float FloatVal;
};
var private array<TCustomData> CustomData;
var private ScrnAchHandlerBase LastSearchedAchHandler;
var private name LastSearchedStatName;
var private int LastFoundCustomDataIndex;


// Backup data for PlayerReplicationInfo
var int SteamID32;
var int PRI_Score, PRI_Kills, PRI_KillAssists, PRI_Deaths, PRI_StartTime;
var byte PRI_BlameCounter;
var class<KFVeterancyTypes> PRI_ClientVeteranSkill;
var byte PRI_TeamIndex;

function ClientPerkRepLink GetRep()
{
    return class'ScrnClientPerkRepLink'.static.FindMe(PlayerOwner);
}

function string PerkStatStr(out TPerkStats Stats)
{
    return "MEDIC"$Stats.RDamageHealedStat
        @"SUP"$Stats.RWeldingPointsStat$"/"$Stats.RShotgunDamageStat
        @"SS"$Stats.RHeadshotKillsStat
        @"CMD"$Stats.RStalkerKillsStat$"/"$Stats.RBullpupDamageStat
        @"ZERK"$Stats.RMeleeDamageStat
        @"FB"$Stats.RFlameThrowerDamageStat
        @"DEMO"$Stats.RExplosivesDamageStat;
}

function string PerkProgressStr(out TPerkStats InitialStats)
{
    local ClientPerkRepLink L;

    L = GetRep();
    if ( L == none )
        return "";

    return "MEDIC"$(L.RDamageHealedStat - InitialStats.RDamageHealedStat)
        @"SUP"$(L.RWeldingPointsStat - InitialStats.RWeldingPointsStat)$"/"$(L.RShotgunDamageStat - InitialStats.RShotgunDamageStat)
        @"SS"$(L.RHeadshotKillsStat - InitialStats.RHeadshotKillsStat)
        @"CMD"$(L.RStalkerKillsStat - InitialStats.RStalkerKillsStat)$"/"$(L.RBullpupDamageStat - InitialStats.RBullpupDamageStat)
        @"ZERK"$(L.RMeleeDamageStat - InitialStats.RMeleeDamageStat )
        @"FB"$(L.RFlameThrowerDamageStat - InitialStats.RFlameThrowerDamageStat)
        @"DEMO"$(L.RExplosivesDamageStat - InitialStats.RExplosivesDamageStat);
}

// backups only if Stats.bSet = false !!!
function BackupStats(out TPerkStats Stats)
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;
    local int i;
    local string ClassName;

    if ( Stats.bSet )
        return;

    L = GetRep();
    if ( L == none )
        return;

    Stats.bSet = true;
    Stats.RDamageHealedStat        = L.RDamageHealedStat;
    Stats.RWeldingPointsStat       = L.RWeldingPointsStat;
    Stats.RShotgunDamageStat       = L.RShotgunDamageStat;
    Stats.RHeadshotKillsStat       = L.RHeadshotKillsStat;
    Stats.RStalkerKillsStat        = L.RStalkerKillsStat;
    Stats.RBullpupDamageStat       = L.RBullpupDamageStat;
    Stats.RMeleeDamageStat         = L.RMeleeDamageStat;
    Stats.RFlameThrowerDamageStat  = L.RFlameThrowerDamageStat;
    Stats.RExplosivesDamageStat    = L.RExplosivesDamageStat;

    Stats.CustomStats.length = 0;
    for ( S=L.CustomLink; S!=none; S=S.NextLink ) {
        if ( SRCustomProgressInt(S) == none && SRCustomProgressFloat(S) == none )
            continue; // store only int values

        ClassName = GetItemName(String(S.class));
        for ( i=0; i<ExcludeBonusStats.length; ++i ) {
            if ( ClassName ~= ExcludeBonusStats[i] ) {
                i = -1;
                break;
            }
        }
        if ( i == -1 )
            continue;

        i = Stats.CustomStats.length;
        Stats.CustomStats.insert(i, 1);
        Stats.CustomStats[i].CustomStatClass = S.class;
        Stats.CustomStats[i].Progress = S.GetProgressInt();
    }
    //PlayerOwner.ClientMessage(class'ScrnPlayerController'.static.ConsoleColorString("Initial Perks Stats: " $ PerkStatStr(Stats), 255, 1, 200));

}

// add bonus values to all stats by multiplying gained progress with Mult, e.g.:
// RDamageHealedStat += (RDamageHealedStat - InitialStats.RDamageHealedStat) * Mult;
// InitialStats.bSet must be true
// New stats will be written into InitialStats
function BonusStats(out TPerkStats InitialStats, float Mult)
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;
    local SRStatsBase SteamStats;
    local int i, v;

    if ( Mult <= 0 )
        return;
    if ( !InitialStats.bSet ) {
        PlayerOwner.ClientMessage(class'ScrnPlayerController'.static.ConsoleColorString("Unable to give end game bonus! No stat backup found", 255, 1, 1));
        return;
    }

    SteamStats = SRStatsBase(PlayerOwner.SteamStatsAndAchievements);
    if ( SteamStats == none )
        return;

    L = SteamStats.Rep;
    if ( L == none )
        return;

    PlayerOwner.ClientMessage(class'ScrnPlayerController'.static.ConsoleColorString("Stat Bonus (x"$Mult$"): " $ PerkProgressStr(InitialStats), 255, 1, 200));

    v = Mult * (L.RDamageHealedStat - InitialStats.RDamageHealedStat);
    if ( v > 0 )
        SteamStats.AddDamageHealed(v);

    v = Mult * (L.RWeldingPointsStat - InitialStats.RWeldingPointsStat);
    if ( v > 0 )
        SteamStats.AddWeldingPoints(v);
    v = Mult * (L.RShotgunDamageStat - InitialStats.RShotgunDamageStat);
    if ( v > 0 )
        SteamStats.AddShotgunDamage(v);

    // that is fucking gay, dude!
    v = Mult * (L.RHeadshotKillsStat - InitialStats.RHeadshotKillsStat);
    while ( v-- > 0 )
        SteamStats.AddHeadshotKill(false);

    v = Mult * (L.RStalkerKillsStat - InitialStats.RStalkerKillsStat);
    while ( v-- > 0 )
        SteamStats.AddStalkerKill();
    v = Mult * (L.RBullpupDamageStat - InitialStats.RBullpupDamageStat);
    if ( v > 0 )
        SteamStats.AddBullpupDamage(v);

    v = Mult * (L.RMeleeDamageStat - InitialStats.RMeleeDamageStat);
    if ( v > 0 )
        SteamStats.AddMeleeDamage(v);

    v = Mult * (L.RFlameThrowerDamageStat - InitialStats.RFlameThrowerDamageStat);
    if ( v > 0 )
        SteamStats.AddFlameThrowerDamage(v);

    v = Mult * (L.RExplosivesDamageStat - InitialStats.RExplosivesDamageStat);
    if ( v > 0 )
        SteamStats.AddExplosivesDamage(v);

    for ( S=L.CustomLink; S!=none; S=S.NextLink ) {
        if ( SRCustomProgressInt(S) == none && SRCustomProgressFloat(S) == none )
            continue; // proceed only int and float values

        for ( i=0; i< InitialStats.CustomStats.length; ++i) {
            if ( InitialStats.CustomStats[i].CustomStatClass == S.Class ) {
                v = Mult * (S.GetProgressInt() - InitialStats.CustomStats[i].Progress);
                if ( v > 0 )
                    S.IncrementProgress(v);
                InitialStats.CustomStats.remove(i, 1);
                break;
            }
        }
    }

    // store modified stats to prevent multiple bonuses
    InitialStats.bSet = false;
    BackupStats(InitialStats);
}


final function bool ProgressAchievement(name AchID, int Inc)
{
    if ( SRStatsBase(PlayerOwner.SteamStatsAndAchievements) == none )
        return false;

    return class'ScrnBalanceSrv.ScrnAchievements'.static.ProgressAchievementByID(
            SRStatsBase(PlayerOwner.SteamStatsAndAchievements).Rep, AchID, Inc);
}

final function ScrnAchievements GetAchievementsByClass(class<ScrnAchievements> AchClass)
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;

    L = GetRep();
    if ( L == none )
        return none;

    for( S = L.CustomLink; S != none; S = S.NextLink ) {
        if( ClassIsChildOf(S.Class, AchClass) )
            return ScrnAchievements(S);
    }
    return none;
}

final function ScrnAchievements GetAchievementsByID(name AchID, out int AchIndex)
{
    local ClientPerkRepLink L;
    local SRCustomProgress S;
    local ScrnAchievements A;
    local int i;

    L = GetRep();
    if ( L == none )
        return none;

    for( S = L.CustomLink; S != none; S = S.NextLink ) {
        A = ScrnAchievements(S);
        if( A != none ) {
            for ( i = 0; i < A.AchDefs.length; ++i ) {
                if ( A.AchDefs[i].ID == AchID ) {
                    AchIndex = i;
                    return A;
                }
            }
        }
    }
    return none;
}


// returns array index or -1, if record not found
protected function int FindCustomData(ScrnAchHandlerBase AchHandler, name StatName, optional bool bCreate)
{
    local int i;

    if ( AchHandler == LastSearchedAchHandler && StatName == LastSearchedStatName )
        return LastFoundCustomDataIndex;

    for ( i=0; i<CustomData.length; ++i ) {
        if ( CustomData[i].AchHandler == AchHandler && CustomData[i].StatName == StatName ) {
            LastSearchedAchHandler = AchHandler;
            LastSearchedStatName = StatName;
            LastFoundCustomDataIndex = i;
            return i;
        }
    }
    if ( bCreate ) {
        CustomData.insert(i, 1);
        CustomData[i].AchHandler = AchHandler;
        CustomData[i].StatName = StatName;
        LastSearchedAchHandler = AchHandler;
        LastSearchedStatName = StatName;
        LastFoundCustomDataIndex = i;
        return i;
    }
    return -1;
}

function bool HasCustomValue(ScrnAchHandlerBase AchHandler, name StatName)
{
    return FindCustomData(AchHandler, StatName) != -1;
}

function string GetCustomString(ScrnAchHandlerBase AchHandler, name StatName)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName);
    if (idx != -1)
        return CustomData[idx].StrVal;
    return "";
}

function int GetCustomValue(ScrnAchHandlerBase AchHandler, name StatName)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName);
    if (idx != -1)
        return CustomData[idx].IntVal;
    return 0;
}

function float GetCustomFloat(ScrnAchHandlerBase AchHandler, name StatName)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName);
    if (idx != -1)
        return CustomData[idx].FloatVal;
    return 0.f;
}

function SetCustomString(ScrnAchHandlerBase AchHandler, name StatName, string Value, optional bool bWaveReset)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName, true);
    CustomData[idx].bWaveReset = bWaveReset;
    CustomData[idx].StrVal = Value;
    CustomData[idx].IntVal = int(Value);
    CustomData[idx].FloatVal = float(Value);
}

function SetCustomValue(ScrnAchHandlerBase AchHandler, name StatName, int Value, optional bool bWaveReset)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName, true);
    CustomData[idx].bWaveReset = bWaveReset;
    CustomData[idx].StrVal = string(Value);
    CustomData[idx].IntVal = Value;
    CustomData[idx].FloatVal = Value;
}

function SetCustomFloat(ScrnAchHandlerBase AchHandler, name StatName, float Value, optional bool bWaveReset)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName, true);
    CustomData[idx].bWaveReset = bWaveReset;
    CustomData[idx].StrVal = string(Value);
    CustomData[idx].IntVal = Value;
    CustomData[idx].FloatVal = Value;
}

// incremets custom progress and returns its new value
function int IncCustomValue(ScrnAchHandlerBase AchHandler, name StatName, int Inc, optional bool bWaveReset)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName, true);
    CustomData[idx].bWaveReset = bWaveReset;
    CustomData[idx].IntVal += Inc;
    CustomData[idx].StrVal = string(CustomData[idx].IntVal);
    CustomData[idx].FloatVal = CustomData[idx].IntVal;
    return CustomData[idx].IntVal;
}

function int IncCustomFloat(ScrnAchHandlerBase AchHandler, name StatName, float Inc, optional bool bWaveReset)
{
    local int idx;

    idx = FindCustomData(AchHandler, StatName, true);
    CustomData[idx].bWaveReset = bWaveReset;
    CustomData[idx].FloatVal += Inc;
    CustomData[idx].StrVal = string(CustomData[idx].FloatVal);
    CustomData[idx].IntVal = CustomData[idx].FloatVal;
    return CustomData[idx].FloatVal;
}


// returns index in WeapInfos
function int RegisterDamageType(KFWeapon Weapon, class<DamageType> DamType, bool bClearPerShotInfo)
{
    local int i;
    local class<KFWeaponDamageType> KFDamType;

    KFDamType = class<KFWeaponDamageType>(DamType);

    if ( Weapon == none || KFDamType == none )
        return -1;

    for ( i=0; i<WeapInfos.Length; ++i ) {
        if ( WeapInfos[i].WeaponClass == Weapon.class && WeapInfos[i].DamType == KFDamType ) {
            WeapInfos[i].Weapon = Weapon;
            if ( bClearPerShotInfo ) {
                WeapInfos[i].KillsPerShot = 0;
                WeapInfos[i].HeadshotsPerShot = 0;
                WeapInfos[i].DecapsPerShot = 0;
                WeapInfos[i].DamagePerShot = 0;
            }
            return i;
        }
    }
    WeapInfos.insert(i, 1);
    WeapInfos[i].Weapon = Weapon;
    WeapInfos[i].WeaponClass = Weapon.class;
    WeapInfos[i].PickupWave = GameRules.Mut.KF.WaveNum;
    // Hack in ScrnHumanPawn forces the game to set Tier3WeaponGiver for all weapons despite their tier.
    // After player kills somebody with this weapon, Tier3WeaponGiver will be set to none in KFGameType.Killed()
    // So only PrevOwner can be used to identify origonal owner
    if ( Weapon.Tier3WeaponGiver != none && Weapon.Tier3WeaponGiver != self ) {
        WeapInfos[i].PrevOwner = Weapon.Tier3WeaponGiver;
        WeapInfos[i].bPrevOwnerDead = Weapon.Tier3WeaponGiver.IsDead();
    }
    WeapInfos[i].DamType = KFDamType;
    WeapInfos[i].TriggerRowHeadshots = 2;
    WeapInfos[i].TriggerHeadshotsPerShot = 2;
    WeapInfos[i].TriggerHeadshotsPerMagazine = 2;
    WeapInfos[i].TriggerKillsPerShot = 2;
    WeapInfos[i].TriggerKillsPerMagazine = 2;
    WeapInfos[i].TriggerDecapsPerShot = 2;
    WeapInfos[i].TriggerDecapsPerMagazine = 2;
    WeapInfos[i].TriggerDamagePerShot = 2;
    WeapInfos[i].TriggerDamagePerMagazine = 2;;
    return i;
}

function ClearWeapInfos()
{
    local int i;

    while ( i<WeapInfos.Length ) {
        WeapInfos[i].HeadshotsPerWave = 0;
        WeapInfos[i].KillsPerWave = 0;
        WeapInfos[i].DecapsPerWave = 0;
        WeapInfos[i].DamagePerWave = 0;
        i++;
    }
}

// calculates all damage types together
// bHeadshot, RowHeadshots and per-shot stats not included and always be returned as 0
function TWeapInfo GetFullWeaponInfo(class<KFWeapon> WC)
{
    local TWeapInfo result;
    local int i;

    for ( i=0; i<WeapInfos.Length; ++i ) {
        if ( WeapInfos[i].WeaponClass == WC ) {
            if ( WeapInfos[i].LastDmgTime > result.LastDmgTime )
                result.LastDmgTime = WeapInfos[i].LastDmgTime;
            result.TotalKills += WeapInfos[i].TotalKills;
            result.TotalHeadshots += WeapInfos[i].TotalHeadshots;
            result.TotalDecaps += WeapInfos[i].TotalDecaps;
            result.TotalDamage += WeapInfos[i].TotalDamage;
            result.HeadshotsPerMagazine += WeapInfos[i].HeadshotsPerMagazine;
            result.HeadshotsPerWave += WeapInfos[i].HeadshotsPerWave;
            result.KillsPerMagazine += WeapInfos[i].KillsPerMagazine;
            result.KillsPerWave += WeapInfos[i].KillsPerWave;
            result.DecapsPerMagazine += WeapInfos[i].DecapsPerMagazine;
            result.DecapsPerWave += WeapInfos[i].DecapsPerWave;
            result.DamagePerMagazine += WeapInfos[i].DamagePerMagazine;
            result.DamagePerWave += WeapInfos[i].DamagePerWave;
        }
    }
    return result;
}

function WeaponFired(KFWeapon W, byte FireMode)
{
    local WeaponFire WF;
    local class<Projectile> proj;

    if ( W == none )
        return;

    LastFiredWeapon = W;
    WF = W.GetFireMode(FireMode);
    if ( WF == none || WF.class == Class'KFMod.NoFire' )
        return;

    proj = WF.default.ProjectileClass;
    if ( proj != none ) {
        RegisterDamageType(W, proj.default.MyDamageType, true);
        if ( class<LAWProj>(proj) != none )
            RegisterDamageType(W, class<LAWProj>(proj).default.ImpactDamageType, true);
        else if ( class<M79GrenadeProjectile>(proj) != none )
            RegisterDamageType(W, class<M79GrenadeProjectile>(proj).default.ImpactDamageType, true);
    }
    if ( InstantFire(WF) != none )
        RegisterDamageType(W, InstantFire(WF).DamageType, true);
    else if ( KFMeleeFire(WF) != none )
        RegisterDamageType(W, KFMeleeFire(WF).hitDamageClass, true);
}

function WeaponReloaded(KFWeapon W)
{
    local int i;
    if ( W == none
    )
        return;

    for ( i=0; i<GameRules.AchHandlers.length; ++i )
        GameRules.AchHandlers[i].WeaponReloaded(self, W);

    for ( i=0; i<WeapInfos.Length; ++i ) {
        if ( WeapInfos[i].Weapon == W ) {
            WeapInfos[i].KillsPerShot = 0;
            WeapInfos[i].KillsPerMagazine = 0;

            WeapInfos[i].DecapsPerShot = 0;
            WeapInfos[i].DecapsPerMagazine = 0;

            WeapInfos[i].DamagePerShot = 0;
            WeapInfos[i].DamagePerMagazine = 0;

            WeapInfos[i].HeadshotsPerShot = 0;
            WeapInfos[i].HeadshotsPerMagazine = 0;
        }
    }
}

function KFWeapon GetCurrentWeapon()
{
    if ( PlayerOwner.Pawn == none || PlayerOwner.Pawn.Health <= 0 )
        return none;

    return KFWeapon(PlayerOwner.Pawn.Weapon);
}


/** Locates WeapInfos record by a given damage type. If there are multiple records with the same
 * damage type, looks for a match with LastFiredWeapon. If such not found, returns last record with
 * matched damage type
 *
 * @param DamType weapon damage type to search
 * @return record index in WeapInfos array or -1, if record not found
*/
function int FindWeaponInfoByDamType(class<KFWeaponDamageType> DamType)
{
    local int i, idx;

    if (  LastWeapInfoIndex >= 0 && LastWeapInfoIndex < WeapInfos.Length
            &&  WeapInfos[LastWeapInfoIndex].DamType == DamType )
        return LastWeapInfoIndex;

    idx = -1;
    for ( i=0; i<WeapInfos.Length; ++i ) {
        if ( WeapInfos[i].DamType == DamType ) {
            // if this damage type can be delivered by last fired weapon, then use it.
            // Otherwice use last registered record with this damage type
            idx = i;
            if ( WeapInfos[i].Weapon == LastFiredWeapon )
                break;
        }
    }
    return idx;
}


function KFWeapon FindWeaponByDamType(class<KFWeaponDamageType> DamType)
{
    local int idx;

    idx = FindWeaponInfoByDamType(DamType);

    if ( idx != -1 )
        return WeapInfos[idx].Weapon;

    return none;
}

/**
 * This function is called each time when player made damage to the monster. P2P damages or damages
 * made by non-KFWeaponDamageType aren't triggered here.
 *
 * @param Damage damage delivered (including perk bonuses, resistance etc.).
 * @param Injured Monster that took damage. Note that Monster.Health isn't touched yet, i.e.
 *          Damage isn't subtracted yet, but Monster.HeadHealth already has actual value.
 * @param DamType damage type.
 * @param bHeadshot is this damage acquired by a headshot? False for any shot made to already decapitaded enemy.
 * @param bWasDecapitated was Injured decapitaded before this shot? If true, bHeadshot will always be false.
 */
function MadeDamage(int Damage, KFMonster Injured, class<KFWeaponDamageType> DamType, bool bHeadshot, bool bWasDecapitated )
{
    local int i, m, v;
    local float t;
    local KFWeapon Weapon;
    local KFPlayerReplicationInfo KFPRI;

    if ( PlayerOwner == none )
        return;
    KFPRI = KFPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);

    LastDamage = Damage;
    LastDamageType = DamType;
    LastDamagedMonster = Injured;
    DamagePerWave += Damage;
    DamagePerGame += Damage;

    self.bHeadshot = bHeadshot;
    if ( bHeadshot ) {
        RowHeadshots++;
        HeadshotsPerWave++;
        HeadshotsPerGame++;
        if ( Injured.bDecapitated ) {
            DecapsPerWave++;
            DecapsPerGame++;
        }
        // EVENT
        if ( RowHeadshots >= TriggerRowHeadshots ) {
            m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
            for ( i=0; i<GameRules.AchHandlers.length; ++i )
                m = min(m, GameRules.AchHandlers[i].RowHeadhots(self, RowHeadshots));
            TriggerRowHeadshots = m;
        }
        // END OF EVENT
    }
    else if ( !bWasDecapitated ) { // don't clear headshots when hitting decapitated bodies
        RowHeadshots = 0;
        if ( DamType.default.bCheckForHeadShots ) {
            BodyshotsPerWave++;
            BodyshotsPerGame++;
        }
    }

    // count medic damage
    if ( ClassIsChildOf(DamType, class'ScrnDamTypeMedicBase')
        || (ClassIsChildOf(DamType, class'DamTypeKatana')
            && KFPRI != none &&  ClassIsChildOf(KFPRI.ClientVeteranSkill, class'ScrnVetFieldMedic')) )
    {
        MedicDamage += min(Damage, Injured.Health);
        if ( MedicDamage >= 1000 ) {
            SRStatsBase(PlayerOwner.SteamStatsAndAchievements).AddDamageHealed(MedicDamage / 1000 * MEDICXP_PER_1000DMG);
            MedicDamage = MedicDamage % 1000;
        }
    }

    LastWeapInfoIndex = FindWeaponInfoByDamType(DamType);
    for ( i=0; i<GameRules.AchHandlers.length; ++i ) {
        GameRules.AchHandlers[i].MonsterDamaged(Damage, Injured, self, DamType, bHeadshot, bWasDecapitated);
    }


    if ( LastWeapInfoIndex != -1 ) {
        Weapon = WeapInfos[LastWeapInfoIndex].Weapon;
        WeapInfos[LastWeapInfoIndex].DamagePerShot += Damage;
        WeapInfos[LastWeapInfoIndex].DamagePerMagazine += Damage;
        WeapInfos[LastWeapInfoIndex].DamagePerWave += Damage;
        WeapInfos[LastWeapInfoIndex].TotalDamage += Damage;
        WeapInfos[LastWeapInfoIndex].bHeadshot = bHeadshot;
        if ( bHeadshot ) {
            WeapInfos[LastWeapInfoIndex].RowHeadshots++;
            WeapInfos[LastWeapInfoIndex].HeadshotsPerShot++;
            WeapInfos[LastWeapInfoIndex].HeadshotsPerMagazine++;
            WeapInfos[LastWeapInfoIndex].HeadshotsPerWave++;
            WeapInfos[LastWeapInfoIndex].TotalHeadshots++;
            if ( Injured.bDecapitated ) {
                WeapInfos[LastWeapInfoIndex].DecapsPerShot++;
                WeapInfos[LastWeapInfoIndex].DecapsPerMagazine++;
                WeapInfos[LastWeapInfoIndex].DecapsPerWave++;
                WeapInfos[LastWeapInfoIndex].TotalDecaps++;
                // EVENT
                if ( WeapInfos[LastWeapInfoIndex].DecapsPerShot >= WeapInfos[LastWeapInfoIndex].TriggerDecapsPerShot ) {
                    m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
                    v = WeapInfos[LastWeapInfoIndex].DecapsPerShot;
                    t = Level.TimeSeconds - WeapInfos[LastWeapInfoIndex].LastDmgTime;
                    for ( i=0; i<GameRules.AchHandlers.length; ++i )
                        m = min(m, GameRules.AchHandlers[i].WDecapsPerShot(self, Weapon, DamType, v, t));
                    WeapInfos[LastWeapInfoIndex].TriggerDecapsPerShot = m;
                }
                if ( WeapInfos[LastWeapInfoIndex].DecapsPerMagazine >= WeapInfos[LastWeapInfoIndex].TriggerDecapsPerMagazine ) {
                    m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
                    v = WeapInfos[LastWeapInfoIndex].DecapsPerMagazine;
                    for ( i=0; i<GameRules.AchHandlers.length; ++i )
                        m = min(m, GameRules.AchHandlers[i].WDecapsPerMagazine(self, Weapon, DamType, v));
                    WeapInfos[LastWeapInfoIndex].TriggerDecapsPerMagazine = m;
                }
                // END OF EVENT
            }
            // EVENT
            if ( WeapInfos[LastWeapInfoIndex].RowHeadshots >= WeapInfos[LastWeapInfoIndex].TriggerRowHeadshots ) {
                m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
                v = WeapInfos[LastWeapInfoIndex].RowHeadshots;
                for ( i=0; i<GameRules.AchHandlers.length; ++i )
                    m = min(m, GameRules.AchHandlers[i].WRowHeadhots(self, Weapon, DamType, v));
                WeapInfos[LastWeapInfoIndex].TriggerRowHeadshots = m;
            }
            if ( WeapInfos[LastWeapInfoIndex].HeadshotsPerShot >= WeapInfos[LastWeapInfoIndex].TriggerHeadshotsPerShot ) {
                m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
                v = WeapInfos[LastWeapInfoIndex].HeadshotsPerShot;
                for ( i=0; i<GameRules.AchHandlers.length; ++i )
                    m = min(m, GameRules.AchHandlers[i].WInstantHeadhots(self, Weapon, DamType, v));
                WeapInfos[LastWeapInfoIndex].TriggerHeadshotsPerShot = m;
            }
            if ( WeapInfos[LastWeapInfoIndex].HeadshotsPerMagazine >= WeapInfos[LastWeapInfoIndex].TriggerHeadshotsPerMagazine ) {
                m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
                v = WeapInfos[LastWeapInfoIndex].HeadshotsPerMagazine;
                for ( i=0; i<GameRules.AchHandlers.length; ++i )
                    m = min(m, GameRules.AchHandlers[i].WHeadshotsPerMagazine(self, Weapon, DamType, v));
                WeapInfos[LastWeapInfoIndex].TriggerHeadshotsPerMagazine = m;
            }
            // END OF EVENT
        }
        else if ( !bWasDecapitated ) {
            WeapInfos[LastWeapInfoIndex].RowHeadshots = 0;
        }
        // EVENT
        if ( WeapInfos[LastWeapInfoIndex].DamagePerShot >= WeapInfos[LastWeapInfoIndex].TriggerDamagePerShot ) {
            m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
            v = WeapInfos[LastWeapInfoIndex].DamagePerShot;
            t = Level.TimeSeconds - WeapInfos[LastWeapInfoIndex].LastDmgTime;
            for ( i=0; i<GameRules.AchHandlers.length; ++i )
                m = min(m, GameRules.AchHandlers[i].WDamagePerShot(self, Weapon, DamType, v, t));
            WeapInfos[LastWeapInfoIndex].TriggerDamagePerShot = m;
        }
        if ( WeapInfos[LastWeapInfoIndex].DamagePerMagazine >= WeapInfos[LastWeapInfoIndex].TriggerDamagePerMagazine ) {
            m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
            v = WeapInfos[LastWeapInfoIndex].DamagePerMagazine;
            for ( i=0; i<GameRules.AchHandlers.length; ++i )
                m = min(m, GameRules.AchHandlers[i].WDamagePerMagazine(self, Weapon, DamType, v));
            WeapInfos[LastWeapInfoIndex].TriggerDamagePerMagazine = m;
        }
        // END OF EVENT

        WeapInfos[LastWeapInfoIndex].LastDmgTime = Level.TimeSeconds;
    }
    LastDmgTime = Level.TimeSeconds;
}

// took damage from the monster
function TookDamage(int Damage, KFMonster InstigatedBy, class<DamageType> DamType)
{
    local int i;

    DamageReceivedPerWave += Damage;
    DamageReceivedPerGame += Damage;

    for ( i=0; i<GameRules.AchHandlers.length; ++i ) {
        GameRules.AchHandlers[i].PlayerDamaged(Damage, self, InstigatedBy, DamType);
    }
}

function KilledMonster(KFMonster Killed, class<KFWeaponDamageType> DamType)
{
    local int i, m, v;
    local float t;
    local KFWeapon Weapon;

    KillsPerWave++;
    LastWeapInfoIndex = FindWeaponInfoByDamType(DamType);
    if ( LastWeapInfoIndex != -1 ) {
        Weapon = WeapInfos[LastWeapInfoIndex].Weapon;
        WeapInfos[LastWeapInfoIndex].KillsPerShot++;
        WeapInfos[LastWeapInfoIndex].KillsPerMagazine++;
        WeapInfos[LastWeapInfoIndex].KillsPerWave++;
        WeapInfos[LastWeapInfoIndex].TotalKills++;

        // EVENT
        if ( WeapInfos[LastWeapInfoIndex].KillsPerShot >= WeapInfos[LastWeapInfoIndex].TriggerKillsPerShot ) {
            m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
            v = WeapInfos[LastWeapInfoIndex].KillsPerShot;
            t = Level.TimeSeconds - WeapInfos[LastWeapInfoIndex].LastKillTime;
            for ( i=0; i<GameRules.AchHandlers.length; ++i )
                m = min(m, GameRules.AchHandlers[i].WKillsPerShot(self, Weapon, DamType, v, t));
            WeapInfos[LastWeapInfoIndex].TriggerKillsPerShot = m;
        }
        if ( WeapInfos[LastWeapInfoIndex].KillsPerMagazine >= WeapInfos[LastWeapInfoIndex].TriggerKillsPerMagazine ) {
            m = IGNORE_STAT; // next time trigger event when reaching minimal returned value
            v = WeapInfos[LastWeapInfoIndex].KillsPerMagazine;
            for ( i=0; i<GameRules.AchHandlers.length; ++i )
                m = min(m, GameRules.AchHandlers[i].WKillsPerMagazine(self, Weapon, DamType, v));
            WeapInfos[LastWeapInfoIndex].TriggerKillsPerMagazine = m;
        }
        // END OF EVENT

        WeapInfos[LastWeapInfoIndex].LastKillTime = Level.TimeSeconds;
    }
    for ( i=0; i<GameRules.AchHandlers.length; ++i ) {
        GameRules.AchHandlers[i].MonsterKilled(Killed, self, DamType);
    }
    LastKillTime = Level.TimeSeconds;
}

function Died(Controller Killer, class<DamageType> DamType)
{
    local int i;

    if ( !bDied ) {
        bDied = true;
        Deaths++;
        LastKilledBy = Killer;
        if ( KFMonster(Killer.Pawn) != none )
            DeathsByMonster++;
        for ( i=0; i<GameRules.AchHandlers.length; ++i ) {
            GameRules.AchHandlers[i].PlayerDied(self, Killer, DamType);
        }
    }
}

/* This player healed anothe one
 * @param HealAmount    the amount of health Patient received
 * @param Patient        player, who received healing
 * @param MedicGun        Instigator's weapon, which was used for healing
 */
function Healed(int HealAmount, ScrnHumanPawn Patient, KFWeapon MedicGun)
{
    local int i;

    HealedPointsInWave += HealAmount;

    for ( i=0; i<GameRules.AchHandlers.length; ++i )
        GameRules.AchHandlers[i].HealingMade(HealAmount, Patient, self, MedicGun);
}

/**
 * Function is called every time after the start of new wave
 * @param WaveNum - wave number, where 0 is first wave
 */
function WaveStarted(byte WaveNum)
{
    local int i;

    ClearWeapInfos();

    for ( i=0; i<CustomData.length; ++i ) {
        if ( CustomData[i].bWaveReset ) {
            CustomData[i].StrVal = "";
            CustomData[i].IntVal = 0;
            CustomData[i].FloatVal = 0;
        }
    }

    bDied = PlayerOwner.Pawn == none || PlayerOwner.Pawn.Health <= 0;
    HeadshotsPerWave = 0;
    BodyshotsPerWave = 0;
    KillsPerWave = 0;
    DecapsPerWave = 0;
    DamagePerWave = 0;
    DamageReceivedPerWave = 0;
    HealedPointsInWave = 0;
    CashDonatedPerWave = 0;
    CashReceivedPerWave = 0;
    CashFoundPerWave = 0;
}

function WaveEnded(byte WaveNum)
{
}

// backup vital player data from KFPRI
function BackupPRI()
{
    local KFPlayerReplicationInfo KFPRI;
    local ScrnCustomPRI ScrnPRI;

    if ( PlayerOwner == none )
        return;

    KFPRI = KFPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);
    if ( KFPRI == none )
        return;
    ScrnPRI = class'ScrnCustomPRI'.static.FindMe(KFPRI);

    if ( !GameRules.Mut.bLeaveCashOnDisconnect )
        PRI_Score = KFPRI.Score;
    PRI_Kills = KFPRI.Kills;
    PRI_KillAssists = KFPRI.KillAssists;
    PRI_Deaths = KFPRI.Deaths;
    PRI_StartTime = KFPRI.StartTime;
    PRI_ClientVeteranSkill = KFPRI.ClientVeteranSkill;

    if ( KFPRI.Team == none )
        PRI_TeamIndex = 255;
    else
        PRI_TeamIndex = KFPRI.Team.TeamIndex;

    if ( ScrnPRI != none ) {
        PRI_BlameCounter = ScrnPRI.BlameCounter;
    }
}

function RestorePRI()
{
    local KFPlayerReplicationInfo KFPRI;
    local ScrnCustomPRI ScrnPRI;

    if ( PlayerOwner == none )
        return;

    KFPRI = KFPlayerReplicationInfo(PlayerOwner.PlayerReplicationInfo);
    if ( KFPRI == none )
        return;
    ScrnPRI = class'ScrnCustomPRI'.static.FindMe(KFPRI);

    if ( KFPRI.Kills == 0 && KFPRI.KillAssists == 0 && KFPRI.Deaths == 0 ) {
        if ( PRI_ClientVeteranSkill != none )
            KFPRI.ClientVeteranSkill = PRI_ClientVeteranSkill;
        if ( PRI_TeamIndex < 2 && ( KFPRI.Team == none || KFPRI.Team.TeamIndex != PRI_TeamIndex ) )
            Level.Game.ChangeTeam( PlayerOwner, PRI_TeamIndex, true );
    }

    if ( !GameRules.Mut.bLeaveCashOnDisconnect )
        KFPRI.Score = max(PRI_Score, KFPRI.Score);
    KFPRI.Kills = max(PRI_Kills, KFPRI.Kills);
    KFPRI.KillAssists = max(PRI_KillAssists, KFPRI.KillAssists);
    KFPRI.Deaths = max(PRI_Deaths, KFPRI.Deaths);
    KFPRI.StartTime = min(PRI_StartTime, KFPRI.StartTime);

    if ( ScrnPRI != none ) {
        ScrnPRI.BlameCounter = max(PRI_BlameCounter, ScrnPRI.BlameCounter);
    }
}


function PickedCash(CashPickup Dosh)
{
    local ScrnPlayerInfo DonatorSPI;
    local int i;

    if ( Dosh.CashAmount == 0 )
        return;

    if ( Dosh.bDroppedCash ) {
        DonatorSPI = GameRules.GetPlayerInfo(PlayerController(Dosh.DroppedBy));
        if ( DonatorSPI == self )
            return; // picked up own dosh
        // dosh was dropped by other player
        CashReceived += Dosh.CashAmount;
        CashReceivedPerWave += Dosh.CashAmount;
        if ( DonatorSPI != none ) {
            DonatorSPI.CashDonated += Dosh.CashAmount;
            DonatorSPI.CashDonatedPerWave += Dosh.CashAmount;
        }
    }
    else {
        // dosh found on the map (wasn't dropped by a player)
        CashFound += Dosh.CashAmount;
        CashFoundPerWave += Dosh.CashAmount;
    }
    // achievements
    for ( i=0; i<GameRules.AchHandlers.length; ++i )
        GameRules.AchHandlers[i].PickedCash(Dosh.CashAmount, self, DonatorSPI, Dosh.bDroppedCash);
}

function PickedWeapon(KFWeaponPickup WeaponPickup)
{
    local int i;

    for ( i=0; i<GameRules.AchHandlers.length; ++i )
        GameRules.AchHandlers[i].PickedWeapon(self, WeaponPickup);
}

function PickedItem(Pickup Item)
{
    local int i;

    for ( i=0; i<GameRules.AchHandlers.length; ++i )
        GameRules.AchHandlers[i].PickedItem(self, Item);
}

function float GetAccuracyWave()
{
    if ( HeadshotsPerWave+BodyShotsPerWave == 0 )
        return 0.f;
    return float(HeadshotsPerWave) / float(HeadshotsPerWave+BodyShotsPerWave);
}

function float GetAccuracyGame()
{
    if ( HeadshotsPerGame+BodyShotsPerGame == 0 )
        return 0.f;
    return float(HeadshotsPerGame) / float(HeadshotsPerGame+BodyShotsPerGame);
}

defaultproperties
{
     MEDICXP_PER_1000DMG=20
     TriggerRowHeadshots=2
     PRI_TeamIndex=255
}
