public static int GetHPerf(this CombatantData data)
        {
            var boss = data.Parent.GetBoss();
            var job  = data.GetJob();

            if (boss == null || job == null)
            {
                return(-1);
            }

            var pHPS        = data.GetPHPS();
            var hPercentile = boss.HPercentiles.Where(x => x.Job == job.Name).FirstOrDefault();

            if (pHPS == -1 || hPercentile == null)
            {
                return(-1);
            }

            return(CalculatePerf(pHPS, hPercentile));
        }
        private static Healing GetHealing(this CombatantData data)
        {
            var job = data.GetJob();

            if (job == null || !data.IsAlly() || data.Name == null)
            {
                return(null);
            }

            var key       = data.Name.ToString();
            var lastSwing = data.AllOut.ContainsKey(AttackType.All) ? data.AllOut[AttackType.All].Items.LastOrDefault() : null;
            var healing   = healingCache.ContainsKey(key) && healingCache[key].LastSwing == lastSwing ? healingCache[key] : null;

            if (healing == null && lastSwing != null &&
                data.Items[DamageTypeData.OutgoingHealing].Items.Count() > 0)
            {
                // Attacks
                var healingItems = data.Items[DamageTypeData.OutgoingHealing].Items[AttackType.All].Items.ToList();
                var attacks      = healingItems.Where(x => x.Damage.Number > 0).ToList();

                var totalHealing = attacks.Select(x => x.Damage.Number - long.Parse((string)x.Tags.GetValue(SwingTag.Overheal, "0"))).Sum();

                healing = new Healing()
                {
                    Value     = totalHealing,
                    LastSwing = lastSwing
                };

                if (healingCache.ContainsKey(key))
                {
                    healingCache[key] = healing;
                }
                else
                {
                    healingCache.Add(key, healing);
                }
            }

            return(healing);
        }
        private static Damage GetDamage(this CombatantData data)
        {
            var job = data.GetJob();

            if (job == null || !data.IsAlly() || data.Name == null)
            {
                return(null);
            }

            var key       = data.Name.ToString();
            var lastSwing = data.AllOut.ContainsKey(AttackType.All) ? data.AllOut[AttackType.All].Items.LastOrDefault() : null;
            var damage    = damageCache.ContainsKey(key) && damageCache[key].LastSwing == lastSwing ? damageCache[key] : null;

            if (damage == null && lastSwing != null &&
                data.Items[DamageTypeData.OutgoingDamage].Items.Count() > 0)
            {
                var allies  = data.Parent.GetAllies().Select(x => x.Name).ToList();
                var enemies = data.Parent.Items.Select(x => x.Value).Where(x => !allies.Contains(x.Name)).ToList();

                // Buff taken
                var buffTakens = new List <BuffTaken>();
                ActGlobalsExtension.Buffs.Where(x => x.Type == BuffType.Buff).ToList().ForEach(buff =>
                {
                    var conflictBuffs = ActGlobalsExtension.Buffs.Where(x => x.IsConflict(buff)).ToArray();
                    buffTakens.AddRange(data.GetBuffTakens(buff, conflictBuffs));
                });

                // Enemies debuff taken
                var enemiesDebuffTakens = new Dictionary <string, List <BuffTaken> >();
                enemies.ForEach(enemy =>
                {
                    var debuffTakens = new List <BuffTaken>();
                    ActGlobalsExtension.Buffs.Where(x => x.Type == BuffType.Debuff).ToList().ForEach(debuff =>
                    {
                        var conflictBuffs = ActGlobalsExtension.Buffs.Where(x => x.IsConflict(debuff)).ToArray();
                        debuffTakens.AddRange(enemy.GetBuffTakens(debuff, conflictBuffs));
                    });
                    enemiesDebuffTakens.Add(enemy.Name, debuffTakens);
                });

                // Attacks
                var damageItems = data.Items[DamageTypeData.OutgoingDamage].Items[AttackType.All].Items.ToList();
                var attacks     = damageItems.Where(x => x.Damage.Number > 0).ToList();

                var criticalRate  = Math.Max(0.01 * data.CritDamPerc, 0.05);
                var directHitRate = 0.01 * data.GetDirectHitRate();

                // Damage
                var totalDamage      = 0.0;
                var buffTakenDamages = new List <BuffTakenDamage>();
                foreach (var attack in attacks)
                {
                    var actorID  = (string)attack.Tags.GetValue(SwingTag.ActorID, "");
                    var targetId = (string)attack.Tags.GetValue(SwingTag.TargetID, "");

                    var damageValue = attack.Damage.Number - long.Parse((string)attack.Tags.GetValue(SwingTag.Overkill, "0"));

                    totalDamage += damageValue;

                    if (attack.Attacker == "Limit Break")
                    {
                        continue;
                    }

                    var skill            = ActGlobalsExtension.Skills.Where(x => x.NameList.Contains(attack.AttackType)).FirstOrDefault();
                    var damageUpRates    = new Dictionary <BuffTaken, double>();
                    var criticalUpRates  = new Dictionary <BuffTaken, double>();
                    var directHitUpRates = new Dictionary <BuffTaken, double>();

                    var attackerBuffTakens = buffTakens.Where(x => (string)x.Swing.Tags.GetValue(SwingTag.TargetID) == actorID).ToList();
                    var victimDebuffTakens = enemiesDebuffTakens
                                             .Where(x => x.Key == attack.Victim).SelectMany(x => x.Value).Where(x => (string)x.Swing.Tags.GetValue(SwingTag.TargetID) == targetId).ToList();
                    attackerBuffTakens.Concat(victimDebuffTakens).Where(x => x.StartTime <= attack.Time && x.EndTime >= attack.Time).ToList()
                    .ForEach(buffTaken =>
                    {
                        var buff = buffTaken.Buff;

                        // Damage
                        var damageUpRate = 0.0;
                        switch (buff.Group)
                        {
                        case BuffGroup.CardForMeleeDPSOrTank:
                            damageUpRate = 0.01 * (job.IsMeleeDPSOrTank() ? 1.0 : 0.5) * buff.DamageRate;
                            break;

                        case BuffGroup.CardForRangedDPSOrHealer:
                            damageUpRate = 0.01 * (job.IsRangedDPSOrHealer() ? 1.0 : 0.5) * buff.DamageRate;
                            break;

                        case BuffGroup.Embolden:
                            if (buffTaken.Swing.Attacker == data.Name)
                            {
                                // Apply only Magical attack
                                damageUpRate = 0.01 * (attack.DamageType.StartsWith("Magic") ? buff.DamageRate : 0);
                            }
                            else
                            {
                                // Apply only physical attack and decrease by time
                                var damageRate = buff.DamageRate - (int)((attack.Time - buffTaken.StartTime).TotalSeconds / 4) * 2;
                                damageUpRate   = 0.01 * (attack.DamageType.StartsWith("Magic") ? 0 : damageRate);
                            }
                            break;

                        default:
                            damageUpRate = 0.01 * buff.DamageRate;
                            break;
                        }
                        if (damageUpRate > 0 && buffTaken.Swing.Attacker != data.Name)
                        {
                            damageUpRates.Add(buffTaken, 1.0 + damageUpRate);
                        }

                        // Critical
                        var criticalUpRate = 0.01 * buff.CriticalRate;
                        if (criticalUpRate > 0)
                        {
                            criticalUpRates.Add(buffTaken, criticalUpRate);
                        }

                        // DirectHit
                        var directHitUpRate = 0.01 * buff.DirectHitRate;
                        if (directHitUpRate > 0)
                        {
                            directHitUpRates.Add(buffTaken, directHitUpRate);
                        }
                    });

                    // Calculate DPS Taken
                    // see: https://www.fflogs.com/help/rdps

                    // M
                    var totalDamageUpRate = 1.0;
                    if (damageUpRates.Count > 0)
                    {
                        totalDamageUpRate = damageUpRates.Values.Aggregate(((y, x) => y *= x));
                    }

                    // L = N - (N / M)
                    var upDamage = damageValue - damageValue / totalDamageUpRate;

                    // Damage
                    if (upDamage > 0)
                    {
                        foreach (KeyValuePair <BuffTaken, double> kv in damageUpRates)
                        {
                            buffTakenDamages.Add(new BuffTakenDamage()
                            {
                                BuffTaken = kv.Key,
                                // gi = L * (log mi / log M)
                                Value = upDamage * Math.Log10(kv.Value) / Math.Log10(totalDamageUpRate),
                                Swing = attack
                            });
                        }
                    }

                    // N' = (N / M)
                    var baseDamageIncludeCDH = damageValue / totalDamageUpRate;

                    // Mc
                    var criticalDamageUpRate = 1.0;
                    if (attack.Critical)
                    {
                        criticalDamageUpRate = 1.4 + criticalRate - 0.05;
                    }

                    // Md
                    var directHitDamageupRate = 1.0;
                    if (bool.Parse((string)attack.Tags.GetValue(SwingTag.DirectHit, "false")))
                    {
                        directHitDamageupRate = 1.25;
                    }

                    // Mdc = Mc * Md
                    var critDHDamageUpRate = criticalDamageUpRate * directHitDamageupRate;

                    // (N' / Mdc)
                    var baseDamage = baseDamageIncludeCDH / critDHDamageUpRate;

                    // (Cb - Cu)
                    var totalCritialUpRate = 0.0;
                    if (criticalUpRates.Count > 0)
                    {
                        totalCritialUpRate = criticalUpRates.Values.Sum();
                    }
                    if (skill != null)
                    {
                        // Apply skill-based increases
                        totalCritialUpRate += 0.01 * skill.CriticalRate;
                    }

                    // Pc = (log Mc / log Mdc) * (N' - (N' / Mdc))
                    var criticalUpDamage = Math.Log10(criticalDamageUpRate) / Math.Log10(critDHDamageUpRate) * (baseDamageIncludeCDH - baseDamage);

                    // Critical
                    if (criticalUpDamage > 0 && totalCritialUpRate < 1.0)
                    {
                        foreach (KeyValuePair <BuffTaken, double> kv in criticalUpRates)
                        {
                            buffTakenDamages.Add(new BuffTakenDamage()
                            {
                                BuffTaken = kv.Key,
                                // gi = (ci / Cb) * Pc
                                Value = kv.Value / (totalCritialUpRate + criticalRate) * criticalUpDamage,
                                Swing = attack
                            });
                        }
                    }

                    // (Db - Cu)
                    var totalDirectHitUpRate = 0.0;
                    if (directHitUpRates.Count > 0)
                    {
                        totalDirectHitUpRate = directHitUpRates.Values.Sum();
                    }
                    if (skill != null)
                    {
                        // Apply skill-based increases
                        totalDirectHitUpRate += 0.01 * skill.DirectHitRate;
                    }

                    // Pd = (log 1.25 / log Mdc) * (N' - (N' / Mdc))
                    var directHitUpDamage = Math.Log10(directHitDamageupRate) / Math.Log10(critDHDamageUpRate) * (baseDamageIncludeCDH - baseDamage);

                    // DirectHit
                    if (directHitUpDamage > 0 && totalDirectHitUpRate < 1.0)
                    {
                        foreach (KeyValuePair <BuffTaken, double> kv in directHitUpRates)
                        {
                            buffTakenDamages.Add(new BuffTakenDamage()
                            {
                                BuffTaken = kv.Key,
                                // gi = (di / Db) * Pd
                                Value = kv.Value / (totalDirectHitUpRate + directHitRate) * directHitUpDamage,
                                Swing = attack
                            });
                        }
                    }
                }

                damage = new Damage()
                {
                    Value            = totalDamage,
                    BuffTakenDamages = buffTakenDamages,
                    LastSwing        = lastSwing
                };

                if (damageCache.ContainsKey(key))
                {
                    damageCache[key] = damage;
                }
                else
                {
                    damageCache.Add(key, damage);
                }
            }

            return(damage);
        }