diff --git a/Source/CombatExtended/CombatExtended/ArmorUtilityCE.cs b/Source/CombatExtended/CombatExtended/ArmorUtilityCE.cs index f6cafbdf4b..a638706384 100644 --- a/Source/CombatExtended/CombatExtended/ArmorUtilityCE.cs +++ b/Source/CombatExtended/CombatExtended/ArmorUtilityCE.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -12,125 +12,199 @@ public static class ArmorUtilityCE { #region Constants - private const float PenetrationRandVariation = 0.05f; // Armor penetration will be randomized by +- this amount - private const float SoftArmorMinDamageFactor = 0.2f; // Soft body armor will always take at least original damage * this number from sharp attacks - private const float HardArmorDamageFactor = 0.5f; // Hard body armor final health damage multiplier - private const float SpikeTrapAPModifierBlunt = 0.65f; - private const float BulletImpactForceMultiplier = 0.2f; //Bullet has less mass => less impluse comparing to melee => less Blunt penetration + // Soft armor will at a minimum take damage from sharp attacks' damage + // multiplied by this factor + private const float SOFT_ARMOR_MIN_SHARP_DAMAGE_FACTOR = 0.2f; + + // Hard armor will take damage from attacks' damage multiplied by this factor + // Essentially a health multiplier for hard armor + private const float HARD_ARMOR_DAMAGE_FACTOR = 0.5f; + + // How much damage of a spike trap should be taken as blunt penetration + private const float TRAP_BLUNT_PEN_FACTOR = 0.65f; + + // Shields reduce whatever blunt penetration they pass off from projectiles + // by multiplying it by this factor + private const float PROJECTILE_SHIELD_BLUNT_PEN_FACTOR = 0.2f; + + // Things taking in damage from a parry will have it multiplied by this factor + private const float PARRY_THING_DAMAGE_FACTOR = 0.5f; #endregion #region Properties + // Used as a constant, defines what stuff makes armor be considered soft + // Note that only armor which is stuffed is checked + private static readonly StuffCategoryDef[] SOFT_STUFFS = { + StuffCategoryDefOf.Fabric, StuffCategoryDefOf.Leathery + }; + + #endregion + + #region Classes + + // Responsible for storing internal information used in armor penetration calculation + internal class AttackInfo + { + // Attack's damage info. Should remain mostly unchanged until the attack is being applied + public DamageInfo dinfo; + // How much penetration force does the attack currently have + public float penAmount = 0f; + // How much damage is in a unit of penetration + public float dmgPerPenAmount = 0f; + // How much of the next attack's penetration is in a unit of this attack's penetration + // Blocked penetration amount is transfered to the next attack multiplied by this value + public float penTransferRate = 0f; + // To which next attack does the current attack decay. Essentially a linked-list + public AttackInfo next; + + public AttackInfo(DamageInfo info) + { + dinfo = info; + penAmount = dinfo.ArmorPenetrationInt; + dmgPerPenAmount = penAmount > 0f + ? dinfo.Amount / penAmount + : 0f; + } + + public void Append(AttackInfo info) + { + var tail = this; + while (tail.next != null) + { + tail = tail.next; + } + tail.next = info; + tail.penTransferRate = tail.dinfo.ArmorPenetrationInt > 0f + ? info.dinfo.ArmorPenetrationInt / tail.dinfo.ArmorPenetrationInt + : 0f; + info.penAmount = 0f; + } + + public void Append(DamageInfo info) + { + Append(new AttackInfo(info)); + } + + // Gets the first attack info that still has some penetration amount, + // or the last element in the linked list + public AttackInfo FirstValidOrLast() + { + AttackInfo newHead = this; + while (newHead.next != null && newHead.penAmount <= 0f) + { + newHead = newHead.next; + } + return newHead; + } - private static readonly StuffCategoryDef[] softStuffs = { StuffCategoryDefOf.Fabric, StuffCategoryDefOf.Leathery }; + public override string ToString() + { + return $"({base.ToString()} dinfo={dinfo} penAmount={penAmount}) " + + $"dmgPerPenAmount={dmgPerPenAmount} penTransferRate={penTransferRate}"; + } + } #endregion #region Methods /// - /// Calculates damage through armor, depending on damage type, target and natural resistance. Also calculates deflection and adjusts damage type and impacted body part accordingly. + /// Calculates damage through armor, depending on damage type, target and natural resistance. + /// Also calculates deflection and adjusts damage type and impacted body part accordingly. /// - /// The pre-armor damage info + /// The pre-armor damage info /// The damaged pawn - /// The pawn's body part that has been hit - /// Whether sharp damage was deflected by armor - /// Returns true if attack did not penetrate pawn's melee shield /// Whether the attack was completely absorbed by the armor - /// If shot is deflected returns a new dinfo cloned from the original with damage amount, Def and ForceHitPart adjusted for deflection, otherwise a clone with only the damage adjusted - public static DamageInfo GetAfterArmorDamage(DamageInfo originalDinfo, Pawn pawn, BodyPartRecord hitPart, out bool armorDeflected, out bool armorReduced, out bool shieldAbsorbed) + /// Whether sharp damage was deflected by armor + /// Whether secondary damage application should be skipped + /// + /// The same damage info with its damage and armor penetration changed, + /// or a different damage info if the original damage info was stopped. + /// + public static DamageInfo GetAfterArmorDamage(DamageInfo origDinfo, Pawn pawn, + out bool armorDeflected, out bool armorReduced, out bool skipSecondary) { - shieldAbsorbed = false; armorDeflected = false; - armorReduced = false; + armorReduced = false; // Unused + skipSecondary = false; - var deflectionComp = pawn.TryGetComp(); - - if (originalDinfo.Def.armorCategory == null - || (!(originalDinfo.Weapon?.projectile is ProjectilePropertiesCE) + if (origDinfo.Def.armorCategory == null + || (!(origDinfo.Weapon?.projectile is ProjectilePropertiesCE) && Verb_MeleeAttackCE.LastAttackVerb == null - && originalDinfo.Weapon == null - && originalDinfo.Instigator == null)) + && origDinfo.Weapon == null + && origDinfo.Instigator == null)) { - return originalDinfo; + return origDinfo; } - var dinfo = new DamageInfo(originalDinfo); - var dmgAmount = dinfo.Amount; - var involveArmor = dinfo.Def.harmAllLayersUntilOutside || hitPart.depth == BodyPartDepth.Outside; - bool isAmbientDamage = dinfo.IsAmbientDamage(); + var dinfo = new DamageInfo(origDinfo); - // In case of ambient damage (fire, electricity) we apply a percentage reduction formula based on the sum of all applicable armor - if (isAmbientDamage) + if (dinfo.IsAmbientDamage()) { - dinfo.SetAmount(Mathf.CeilToInt(GetAmbientPostArmorDamage(dmgAmount, originalDinfo.Def.armorCategory.armorRatingStat, pawn, hitPart))); + dinfo.SetAmount(GetAmbientPostArmorDamage( + dinfo.Amount, + dinfo.ArmorRatingStat(), + pawn, + dinfo.HitPart)); armorDeflected = dinfo.Amount <= 0; return dinfo; } - else if (deflectionComp != null) + + var deflectionComp = pawn.TryGetComp(); + if (deflectionComp != null) { deflectionComp.deflectedSharp = false; } - var penAmount = originalDinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo); + // Initialize the AttackInfo linked list + var ainfo = new AttackInfo(dinfo); + if (dinfo.ArmorRatingStat() == StatDefOf.ArmorRating_Sharp) + { + ainfo.Append(GetDeflectDamageInfo(dinfo)); + } + // Remember the damage worker for later use + var dworker = dinfo.Def.Worker; - // Apply worn armor + var involveArmor = ainfo.dinfo.Def.harmAllLayersUntilOutside + || ainfo.dinfo.HitPart.depth == BodyPartDepth.Outside; if (involveArmor && pawn.apparel != null && !pawn.apparel.WornApparel.NullOrEmpty()) { var apparel = pawn.apparel.WornApparel; + Apparel app; - // Check for shields first - var shield = apparel.FirstOrDefault(x => x is Apparel_Shield); - if (shield != null) + // Check against shield + app = apparel.FirstOrDefault(x => x is Apparel_Shield); + if (app != null && (!dinfo.Weapon?.IsMeleeWeapon ?? true) + && DoesShieldCover(ainfo.dinfo.HitPart, pawn, app)) { - // Determine whether the hit is blocked by the shield - var blockedByShield = false; - if (!(dinfo.Weapon?.IsMeleeWeapon ?? false)) + // Apply attack infos + for (var info = ainfo; + info != null && app != null && !app.Destroyed; + info = info.next) { - var shieldDef = shield.def.GetModExtension(); - if (shieldDef == null) + var blockedPenAmount = PenetrateArmor(info, app); + // Blocked penetration amount decays into next attack penetration + if (info.next != null) { - Log.ErrorOnce("Combat Extended :: shield " + shield.def.ToString() + " is Apparel_Shield but has no ShieldDefExtension", shield.def.GetHashCode() + 12748102); - } - else - { - var hasCoverage = shieldDef.PartIsCoveredByShield(hitPart, pawn); - if (hasCoverage) - { - // Right arm is vulnerable during warmup/attack/cooldown - blockedByShield = !((pawn.stances?.curStance as Stance_Busy)?.verb != null && hitPart.IsInGroup(CE_BodyPartGroupDefOf.RightArm)); - } + info.next.penAmount += info.penTransferRate * blockedPenAmount; } } - // Try to penetrate the shield - if (blockedByShield && !TryPenetrateArmor(dinfo.Def, shield.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount, shield)) + // Primary attack was exhausted + if (ainfo.penAmount <= 0f) { - //Deflected by sharp armor, check for blunt armor - if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp) + // Apply secondary damage to shield + if (!app.Destroyed) { - if (deflectionComp != null) - { - deflectionComp.deflectedSharp = true; - } - } - // Hit was deflected, convert damage type - dinfo = GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount); - //Applies multiplier to bullet. - penAmount *= dinfo.Weapon?.IsMeleeWeapon ?? false ? 1.0f : BulletImpactForceMultiplier; - //Check if converted blunt damage could penetrate the shield - if (!TryPenetrateArmor(dinfo.Def, shield.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount)) - { - shieldAbsorbed = true; - armorDeflected = true; - dinfo.SetAmount(0); - // Apply secondary damage to shield - if (dinfo.Weapon?.projectile is ProjectilePropertiesCE props && !props.secondaryDamage.NullOrEmpty()) + skipSecondary = true; + if (ainfo.dinfo.Weapon?.projectile is ProjectilePropertiesCE props + && !props.secondaryDamage.NullOrEmpty()) { foreach (var sec in props.secondaryDamage) { - if (shield.Destroyed) + if (app.Destroyed) { break; } @@ -138,425 +212,451 @@ public static DamageInfo GetAfterArmorDamage(DamageInfo originalDinfo, Pawn pawn { continue; } - var secDinfo = sec.GetDinfo(); - var pen = secDinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo); - var dmg = (float)secDinfo.Amount; - TryPenetrateArmor(secDinfo.Def, shield.GetStatValue(secDinfo.Def.armorCategory.armorRatingStat), ref pen, ref dmg, shield); + PenetrateArmor(new AttackInfo(sec.GetDinfo()), app); } } + } - return dinfo; + // Update deflection comp + if (ainfo.dinfo.ArmorRatingStat() == StatDefOf.ArmorRating_Sharp + && deflectionComp != null) + { + deflectionComp.deflectedSharp = true; } - //Blunt damage penetrated the shield, apply the damage to left arm - //Could add a check for having weapon equipped, if not, pawns should be able to hold the shield with both arms, increasing their defence - else + + // Update primary attack + ainfo = ainfo.FirstValidOrLast(); + // All attacks were exhausted; return + if (ainfo.penAmount <= 0f) { - shieldAbsorbed = true; - //Priority: Left Arm > Right Arm > Left Shoulder - //It seems that losing left shoulder makes shield unequippable, so no need to add a null check (for now) - //FirstOrFallback returns null when nothing satisfies, effectively brings the program into the if statement - BodyPartRecord PartToHit = pawn.health.hediffSet.GetNotMissingParts(depth: BodyPartDepth.Outside, tag: BodyPartTagDefOf.ManipulationLimbCore).FirstOrFallback(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftArm) || x.IsInGroup(CE_BodyPartGroupDefOf.RightArm)); - if (PartToHit == null) - { - PartToHit = pawn.health.hediffSet.GetNotMissingParts(depth: BodyPartDepth.Outside, tag: BodyPartTagDefOf.ManipulationLimbSegment).First(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftShoulder)); - } - dinfo.SetHitPart(PartToHit); - dinfo.SetAmount(dmgAmount); - // Apply secondary damage to shield - if (dinfo.Weapon?.projectile is ProjectilePropertiesCE props && !props.secondaryDamage.NullOrEmpty()) + ainfo.dinfo.SetAmount(0f); + return ainfo.dinfo; + } + + // Update target body part for blunt attack infos + BodyPartRecord partToHit = pawn.health.hediffSet.GetNotMissingParts( + depth: BodyPartDepth.Outside) + .FirstOrFallback(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftArm) + || x.IsInGroup(CE_BodyPartGroupDefOf.RightArm)); + // Cannot wear shields without a left shoulder + if (partToHit == null) + { + partToHit = pawn.health.hediffSet.GetNotMissingParts( + depth: BodyPartDepth.Outside) + .First(x => x.IsInGroup(CE_BodyPartGroupDefOf.LeftShoulder)); + } + + for (var info = ainfo; info != null; info = info.next) + { + if (ainfo.dinfo.Def.armorCategory == CE_DamageArmorCategoryDefOf.Blunt) { - foreach (var sec in props.secondaryDamage) - { - if (shield.Destroyed) - { - break; - } - if (!Rand.Chance(sec.chance)) - { - continue; - } - var secDinfo = sec.GetDinfo(); - var pen = secDinfo.ArmorPenetrationInt; //GetPenetrationValue(originalDinfo); - var dmg = (float)secDinfo.Amount; - TryPenetrateArmor(secDinfo.Def, shield.GetStatValue(secDinfo.Def.armorCategory.armorRatingStat), ref pen, ref dmg, shield); - } + ainfo.dinfo.SetHitPart(partToHit); } - return dinfo; } - } } - // Apparel is arranged in draw order, we run through reverse to go from Shell -> OnSkin + // Check against apparel + // Apparel is arranged in draw order, iterate in reverse to go Shell -> OnSkin for (var i = apparel.Count - 1; i >= 0; i--) { - var app = apparel[i]; - if (app != null - && app.def.apparel.CoversBodyPart(hitPart) - && !TryPenetrateArmor(dinfo.Def, app.PartialStat(dinfo.Def.armorCategory.armorRatingStat, hitPart), ref penAmount, ref dmgAmount, app)) + app = apparel[i]; + if (app == null + || !app.def.apparel.CoversBodyPart(dinfo.HitPart) + || app is Apparel_Shield) { - if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp) - { - if (deflectionComp != null) - { - deflectionComp.deflectedSharp = true; - } - } + continue; + } - // Hit was deflected, convert damage type - //armorReduced = true; - dinfo = GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount); - if (app == apparel.ElementAtOrDefault(i)) //Check whether the "deflecting" apparel is still in the WornApparel - if not, the next loop checks again and errors out because the index is out of range + // Apply attack infos + for (var info = ainfo; + info != null && app != null && !app.Destroyed; + info = info.next) + { + var blockedPenAmount = PenetrateArmor(info, app); + // Blocked penetration amount decays into next attack penetration + if (info.next != null) { - i++; // We apply this piece of apparel twice on conversion, this means we can't use deflection on Blunt or else we get an infinite loop of eternal deflection + info.next.penAmount += info.penTransferRate * blockedPenAmount; } } - if (dmgAmount <= 0) + + // Primary attack was exhausted + if (ainfo.penAmount <= 0f) { - dinfo.SetAmount(0); - armorDeflected = true; - return dinfo; + // Update deflection comp + if (ainfo.dinfo.ArmorRatingStat() == StatDefOf.ArmorRating_Sharp + && deflectionComp != null) + { + deflectionComp.deflectedSharp = true; + } + + // Update primary attack + ainfo = ainfo.FirstValidOrLast(); + // All attacks were exhausted; return + if (ainfo.penAmount <= 0f) + { + ainfo.dinfo.SetAmount(0f); + return ainfo.dinfo; + } } } } - // Apply natural armor - var partsToHit = new List() - { - hitPart - }; - if (dinfo.Def.harmAllLayersUntilOutside) + // Apply every attack info to every body part it should hit + var sharpPartDensity = pawn.GetStatValue(CE_StatDefOf.BodyPartSharpArmor); + var bluntPartDensity = pawn.GetStatValue(CE_StatDefOf.BodyPartBluntArmor); + for (var info = ainfo; info != null; info = info.next) { - var curPart = hitPart; - while (curPart.parent != null && curPart.depth == BodyPartDepth.Inside) + var partsToHit = new List() { - curPart = curPart.parent; - partsToHit.Add(curPart); - } - } - - var isSharp = dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp; - var partDensityStat = isSharp - ? CE_StatDefOf.BodyPartSharpArmor - : CE_StatDefOf.BodyPartBluntArmor; - var partDensity = pawn.GetStatValue(partDensityStat); // How much armor is provided by sheer meat - for (var i = partsToHit.Count - 1; i >= 0; i--) - { - var curPart = partsToHit[i]; - var coveredByArmor = curPart.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor); - var armorAmount = coveredByArmor ? pawn.PartialStat(dinfo.Def.armorCategory.armorRatingStat, curPart, dmgAmount, penAmount) : 0; + info.dinfo.HitPart + }; - // Only apply damage reduction when penetrating armored body parts - if (!TryPenetrateArmor(dinfo.Def, armorAmount, ref penAmount, ref dmgAmount, null, partDensity)) + // Whether or not to hit parent parts of attack hitpart + if (info.dinfo.HitPart.depth == BodyPartDepth.Inside + && info.dinfo.Def.harmAllLayersUntilOutside + && info.dinfo.AllowDamagePropagation) { - dinfo.SetHitPart(curPart); - if (isSharp && coveredByArmor) + info.dinfo.SetAllowDamagePropagation(false); + var partToHit = info.dinfo.HitPart; + while (partToHit.parent != null && partToHit.depth == BodyPartDepth.Inside) { - if (dinfo.Def.armorCategory.armorRatingStat == StatDefOf.ArmorRating_Sharp) - { - if (deflectionComp != null) - { - deflectionComp.deflectedSharp = true; - - deflectionComp.weapon = originalDinfo.Weapon; - } - } - // For Mechanoid natural armor, apply deflection and blunt armor - dinfo = GetDeflectDamageInfo(dinfo, curPart, ref dmgAmount, ref penAmount); - - // Fetch armor rating stat again in case of deflection conversion to blunt - TryPenetrateArmor(dinfo.Def, pawn.GetStatValue(dinfo.Def.armorCategory.armorRatingStat), ref penAmount, ref dmgAmount, null, partDensity); + partToHit = partToHit.parent; + partsToHit.Add(partToHit); } - break; } - if (dmgAmount <= 0) + + var armorRatingStat = ainfo.dinfo.ArmorRatingStat(); + var partDensity = 0f; + if (armorRatingStat == StatDefOf.ArmorRating_Sharp) { - dinfo.SetAmount(0); - armorDeflected = true; - return dinfo; + partDensity = sharpPartDensity; + } + else if (armorRatingStat == StatDefOf.ArmorRating_Blunt) + { + partDensity = bluntPartDensity; } - } - // Applies blunt damage from partial penetrations. - if (isSharp && (dinfo.Amount > dmgAmount)) - { - pawn.TakeDamage(GetDeflectDamageInfo(dinfo, hitPart, ref dmgAmount, ref penAmount, true)); - } - // Return damage info. - dinfo.SetAmount(dmgAmount); - return dinfo; - } - /// - /// Calculates armor for penetrating damage types (Blunt, Sharp). Applies damage reduction based on armor penetration to armor ratio and calculates damage accordingly, with the difference being applied to the armor Thing. Also calculates whether a Sharp attack is deflected. - /// - /// The DamageDef of the attack - /// The amount of armor to apply - /// How much penetration the attack still has - /// The pre-armor amount of damage - /// The armor apparel - /// When penetrating body parts, the body part density - /// False if the attack is deflected, true otherwise - private static bool TryPenetrateArmor(DamageDef def, float armorAmount, ref float penAmount, ref float dmgAmount, Thing armor = null, float partDensity = 0) - { - // Calculate deflection - var isSharpDmg = def.armorCategory == DamageArmorCategoryDefOf.Sharp; - var isFireDmg = def.armorCategory == CE_DamageArmorCategoryDefOf.Heat; - //var rand = UnityEngine.Random.Range(penAmount - PenetrationRandVariation, penAmount + PenetrationRandVariation); - var deflected = isSharpDmg && armorAmount > penAmount; - - // Apply damage reduction - var defCE = def.GetModExtension() ?? new DamageDefExtensionCE(); - var noDmg = deflected && defCE.noDamageOnDeflect; - var newPenAmount = penAmount - armorAmount; - - var dmgMult = noDmg ? 0 : penAmount == 0 ? 1 : Mathf.Clamp01(newPenAmount / penAmount); - deflected = deflected || dmgMult == 0; - var newDmgAmount = dmgAmount * dmgMult; - newPenAmount -= partDensity; // Factor partDensity only after damage calculations - - // Apply damage to armor - if (armor != null) - { - var isSoftArmor = armor.Stuff != null && armor.Stuff.stuffProps.categories.Any(s => softStuffs.Contains(s)); - float armorDamage = 0; - if (isSoftArmor) + // Go over partsToHit from outside to inside + for (var i = partsToHit.Count - 1; i >= 0 && info.penAmount >= 0f; i--) { - // Soft armor takes absorbed damage from sharp and no damage from blunt - if (isFireDmg) - { - armorDamage = armor.GetStatValue(StatDefOf.Flammability, true) * dmgAmount; - } - if (isSharpDmg) - { - armorDamage = Mathf.Max(dmgAmount * SoftArmorMinDamageFactor, dmgAmount - newDmgAmount); - TryDamageArmor(def, penAmount, armorAmount, ref armorDamage, armor); - } + var partToHit = partsToHit[i]; + // Don't apply body part density if it's the first part + PenetrateBodyPart(info, dworker, pawn, partToHit, partDensity); } - else + } + + // Primary attack was exhausted + if (ainfo.penAmount <= 0f) + { + // Update deflection comp + if (ainfo.dinfo.ArmorRatingStat() == StatDefOf.ArmorRating_Sharp + && deflectionComp != null) { - // Hard armor takes damage depending on the damage amount and damage penetration - // Such armor takes the most damage when the attack has the same amount of armor penetration as the armor has armor amount - // Otherwise it's a non-penetration (was unable to perforate the armor) or an over-penetration (creates a nice hole in the armor) - // It is assumed that elastic deformation (no damage) occurs when the attack is blunt and has less armor penetration than the armor amount divided by 2 - if (!isSharpDmg && (penAmount / armorAmount) < 0.5f) - { - armorDamage = 0; - } - else - { - if (isFireDmg) - { - armorDamage = armor.GetStatValue(StatDefOf.Flammability, true) * dmgAmount; - } - else - { - if (penAmount == 0 || armorAmount == 0) - { - if (armor.GetStatValue(StatDefOf.ArmorRating_Sharp) == 0 && armor.GetStatValue(StatDefOf.ArmorRating_Blunt) == 0 && armor.GetStatValue(StatDefOf.ArmorRating_Heat) == 0) - { - Log.ErrorOnce($"penAmount or armorAmount are zero for {def.armorCategory} on {armor}", armor.def.GetHashCode() + 846532021); - } - } - else - { - armorDamage = (dmgAmount - newDmgAmount) * Mathf.Min(1.0f, (penAmount * penAmount) / (armorAmount * armorAmount)) + newDmgAmount * Mathf.Clamp01(armorAmount / penAmount); - } - } - armorDamage *= HardArmorDamageFactor; - } + deflectionComp.deflectedSharp = true; + } - TryDamageArmor(def, penAmount, armorAmount, ref armorDamage, armor); + // Update primary attack + ainfo = ainfo.FirstValidOrLast(); + // All attacks were exhausted; return + if (ainfo.penAmount <= 0f) + { + ainfo.dinfo.SetAmount(0f); + return ainfo.dinfo; } } - if (!deflected || !isSharpDmg) + // Apply all other attacks that have any penetration amount + for (var info = ainfo.next; info != null; info = info.next) { - dmgAmount = Mathf.Max(0, newDmgAmount); - penAmount = Mathf.Max(0, newPenAmount); + if (info.penAmount <= 0f) + { + continue; + } + + info.dinfo.SetIgnoreArmor(true); // Prevents recursion + info.dinfo.weaponInt = null; // Prevents secondary damage + info.dinfo.SetAmount(info.penAmount * info.dmgPerPenAmount); + // Setting armor penetration amount just in case + info.dinfo.armorPenetrationInt = info.penAmount; + // Apply damage using the damage worker we remembered at the start + dworker.Apply(info.dinfo, pawn); } - return !deflected; + + ainfo.dinfo.SetAmount(ainfo.penAmount * ainfo.dmgPerPenAmount); + // Setting armor penetration amount just in case + ainfo.dinfo.armorPenetrationInt = ainfo.penAmount; + return ainfo.dinfo; } /// - /// Damages the armor, where the damage amount is modified, with damages rounding either to ceiling or floor based off of the fraction of the number and damages less than 1 having an increased chance to round to ceiling based off the ratio of armor penetration to armor amount. + /// Bridge function to pass the apparel armor rating to the full function /// - /// The DamageDef of the attack - /// The amount of armor to apply - /// How much penetration the attack still has - /// The pre-armor amount of damage - /// The armor apparel - /// Returns true if the armor takes damage, false if it doesn't. - private static bool TryDamageArmor(DamageDef def, float penAmount, float armorAmount, ref float armorDamage, Thing armor) + private static float PenetrateArmor(AttackInfo ainfo, Apparel armor) { - if (armorDamage == 0) + if (ainfo.penAmount <= 0f) { - return false; + return 0f; } - // Any fractional armor damage has a chance to get rounded to the largest nearest whole number - // Combined with the previous dice roll, values between 0 and 1 have an increased chance to get rounded up - if (Rand.Value < (armorDamage - Mathf.Floor(armorDamage))) + var armorAmount = armor.PartialStat(ainfo.dinfo.ArmorRatingStat(), ainfo.dinfo.HitPart); + return PenetrateArmor(ainfo, armor, armorAmount); + } + + /// + /// Bridge function to pass the weapon toughness rating to the full function + /// + private static float PenetrateWeapon(AttackInfo ainfo, Thing weapon) + { + if (ainfo.penAmount <= 0f) { - armorDamage = Mathf.Ceil(armorDamage); + return 0f; } - armorDamage = Mathf.Floor(armorDamage); - // Don't call TakeDamage() with 0 damage as that would be a waste - if (armorDamage != 0f) + var armorRatingStat = ainfo.dinfo.ArmorRatingStat(); + float armorAmount = 0f; + if (armorRatingStat == StatDefOf.ArmorRating_Sharp) { - armor.TakeDamage(new DamageInfo(def, armorDamage)); - return true; + armorAmount = weapon.GetStatValue(CE_StatDefOf.ToughnessRating); } - return false; + else if (armorRatingStat == StatDefOf.ArmorRating_Blunt) + { + armorAmount = weapon.GetStatValue(CE_StatDefOf.ToughnessRating) * 1.5f; + } + return PenetrateArmor(ainfo, weapon, armorAmount); } - /// - /// Calculates damage reduction for ambient damage types (fire, electricity) versus natural and worn armor of a pawn. Adds up the total armor percentage (clamped at 0-100%) and multiplies damage by that amount. + /// Reduces the penetration amount of an attack info by the provided armor amount. + /// Damages the armor that's provided. /// - /// The original amount of damage - /// The armor stat to use for damage reduction - /// The damaged pawn - /// The body part affected - /// The post-armor damage ranging from 0 to the original amount - private static float GetAmbientPostArmorDamage(float dmgAmount, StatDef armorRatingStat, Pawn pawn, BodyPartRecord part) + /// Attack info to use to penetrate armor + /// Armor thing to damage + /// The armor amount of the armor + /// The amount of penetration amount that was blocked + private static float PenetrateArmor(AttackInfo ainfo, Thing armor, float armorAmount) { - var dmgMult = 1f; - if (part.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor)) + if (ainfo.penAmount <= 0f) { - dmgMult -= pawn.GetStatValue(armorRatingStat); + return 0f; } - if (dmgMult <= 0) + var penAmount = ainfo.penAmount; + var blockedPenAmount = penAmount < armorAmount ? penAmount : armorAmount; + var newPenAmount = penAmount - blockedPenAmount; + + + var isSoftArmor = armor.Stuff?.stuffProps.categories.Any(s => SOFT_STUFFS.Contains(s)) + ?? false; + var dmgAmount = penAmount * ainfo.dmgPerPenAmount; + var blockedDmgAmount = blockedPenAmount * ainfo.dmgPerPenAmount; + var newDmgAmount = newPenAmount * ainfo.dmgPerPenAmount; + + var armorRatingStat = ainfo.dinfo.ArmorRatingStat(); + var armorDamage = 0f; + if (armorRatingStat == StatDefOf.ArmorRating_Heat) { - return 0; + armorDamage = armor.GetStatValue(StatDefOf.Flammability, true) * dmgAmount; } - if (pawn.apparel != null && !pawn.apparel.WornApparel.NullOrEmpty()) + // Soft armor takes only blocked damage from sharp + else if (isSoftArmor) { - var apparelList = pawn.apparel.WornApparel; - foreach (var apparel in apparelList) + if (armorRatingStat == StatDefOf.ArmorRating_Sharp) { - if (apparel.def.apparel.CoversBodyPart(part)) - { - dmgMult -= apparel.GetStatValue(armorRatingStat); - } - if (dmgMult <= 0) - { - dmgMult = 0; - break; - } + armorDamage = Mathf.Max(dmgAmount * SOFT_ARMOR_MIN_SHARP_DAMAGE_FACTOR, + blockedDmgAmount); } } - - var deflectionComp = pawn.TryGetComp(); - if (deflectionComp != null) + // Hard armor takes damage from blocked damage and unblocked damage depending on + // the ratio of penetration amount to armor amount + else { - if (armorRatingStat == StatDefOf.ArmorRating_Heat) + float penArmorRatio = penAmount / armorAmount; + // Armor takes the most sharp damage when penetration amount and armor amount + // are one to one. Less penetration than armor means failure to penetrate, + // higher penetration than armor means over-penetration. + if (armorRatingStat == StatDefOf.ArmorRating_Sharp) { - if (deflectionComp.deflectedSharp) + armorDamage = blockedDmgAmount * Mathf.Clamp01(penArmorRatio * penArmorRatio) + + newDmgAmount * Mathf.Clamp01(1.0f / penArmorRatio); + } + // Blunt damage. It's assumed that elastic deformation occurs when attack is blunt + // and has penetration less than half of armor amount. + else if (armorRatingStat == StatDefOf.ArmorRating_Blunt) + { + if (penArmorRatio > 0.5f) { - dmgMult /= 2f; + penArmorRatio -= 0.5f; + armorDamage = Mathf.Min(dmgAmount, dmgAmount * penArmorRatio * penArmorRatio); } } - deflectionComp.deflectedSharp = false; + + armorDamage *= HARD_ARMOR_DAMAGE_FACTOR; + } + if (armorDamage > 0f) + { + TryDamageArmor(ainfo.dinfo.Def, armorDamage, armor); } - return (float)Math.Floor(dmgAmount * dmgMult); + + ainfo.penAmount = newPenAmount; + return blockedPenAmount; + } + + /// + /// Damages armor by the integer part of the damage amount. + /// The fractional part of the damage amount is a chance to deal one additional damage. + /// + /// True if the armor takes damage, false if it doesn't. + private static bool TryDamageArmor(DamageDef def, float armorDamage, Thing armor) + { + // Fractional damage has a chance to round up or round down + // by the chance of the fractional part. + float flooredDamage = Mathf.Floor(armorDamage); + armorDamage = Rand.Chance(armorDamage - flooredDamage) + ? Mathf.Ceil(armorDamage) + : flooredDamage; + + // Don't call TakeDamage with zero damage + if (armorDamage > 0f) + { + armor.TakeDamage(new DamageInfo(def, (int)armorDamage)); + return true; + } + return false; + } + + /// + /// Reduces the penenetration amount of an attack by the armor amount of a pawn's body part + /// and post armor amount (body part density). Applies additional damage to the body part + /// using a damage worker if it isn't the target hit part of the attack info's damage info. + /// + /// Attack info to use to penetrate the body part + /// Damage worker to use in case of damage to body part + /// Pawn to which the part belongs to + /// Part to penetrate + /// How much armor to apply post penetration + /// The amount of penetration that was blocked + private static float PenetrateBodyPart(AttackInfo ainfo, DamageWorker dworker, + Pawn pawn, BodyPartRecord part, float partDensity) + { + if (ainfo.penAmount <= 0f) + { + return 0f; + } + + var armorAmount = pawn.PartialStat(ainfo.dinfo.ArmorRatingStat(), part); + if (part.depth == BodyPartDepth.Inside) + { + armorAmount += partDensity; + } + + var penAmount = ainfo.penAmount; + var newPenAmount = Mathf.Max(penAmount - armorAmount, 0f); + + if (ainfo.dinfo.HitPart != part) + { + // Use a cloned damage info so as not to polute the attack's damage info + var dinfo = new DamageInfo(ainfo.dinfo); + dinfo.SetHitPart(part); + dinfo.SetIgnoreArmor(true); // Prevent recursion + dinfo.weaponInt = null; // Prevent secondary damage + dinfo.SetAmount(newPenAmount * ainfo.dmgPerPenAmount); + dinfo.armorPenetrationInt = newPenAmount; + dworker.Apply(dinfo, pawn); + } + + ainfo.penAmount = newPenAmount; + return penAmount - newPenAmount; } /// - /// Creates a new DamageInfo from a deflected one. Changes damage type to Blunt and hit part to the outermost parent of the originally hit part. + /// Creates a new DamageInfo from a deflected attack. Changes damage type to Blunt, + /// hit part to the outermost parent of the originally hit part and damage amount. /// /// The dinfo that was deflected - /// The originally hit part - /// Is this is supposed to be a partial penetration /// DamageInfo copied from dinfo with Def and forceHitPart adjusted - private static DamageInfo GetDeflectDamageInfo(DamageInfo dinfo, BodyPartRecord hitPart, ref float dmgAmount, ref float penAmount, bool partialPen = false) + private static DamageInfo GetDeflectDamageInfo(DamageInfo dinfo) { - if (dinfo.Def.armorCategory != DamageArmorCategoryDefOf.Sharp) + DamageInfo newDinfo; + if (dinfo.ArmorRatingStat() != StatDefOf.ArmorRating_Sharp) { - if (!partialPen) - { - dmgAmount = 0; - penAmount = 0; - } - dinfo.SetAmount(0); - return dinfo; + newDinfo = new DamageInfo(dinfo); + newDinfo.SetAmount(0); + return newDinfo; } - //Creating local variables as we don't want to edit the pass-by-reference parameters in the case of partial penetrations. - float localDmgAmount = dmgAmount; - float localPenAmount = penAmount; - - //Calculating blunt damage from sharp damage: if it's a deflected sharp attack, then the penetration amount is directly localPenAmount. - //However, if it's a partially-penetrating sharp attack, then we're using the blocked values of penetration amount and damage amount instead - //and because localPenAmount is the sharp attack's remaining penetration amount and localDmgAmount is the sharp attack's remaining damage amount, - //we have to take that amount away from the base penetration amount and damage amount. - float penMulti = (partialPen ? ((dinfo.ArmorPenetrationInt - localPenAmount) * (dinfo.Amount - localDmgAmount) / dinfo.Amount) : localPenAmount) / dinfo.ArmorPenetrationInt; + var penAmount = 0f; + var dmgMulti = 1f; if (dinfo.Weapon?.projectile is ProjectilePropertiesCE projectile) { - localPenAmount = projectile.armorPenetrationBlunt * penMulti; + penAmount = projectile.armorPenetrationBlunt; + if (projectile.damageAmountBase != 0) + { + dmgMulti = dinfo.Amount / (float)projectile.damageAmountBase; + } } else if (dinfo.Instigator?.def.thingClass == typeof(Building_TrapDamager)) { - //Temporarily deriving spike trap blunt AP based on their vanilla stats, just so they're not entirely broken - //TODO proper integration - var trapAP = dinfo.Instigator.GetStatValue(StatDefOf.TrapMeleeDamage, true) * SpikeTrapAPModifierBlunt; - localPenAmount = trapAP * penMulti; + penAmount = dinfo.Instigator.GetStatValue(StatDefOf.TrapMeleeDamage, true) + * TRAP_BLUNT_PEN_FACTOR; } - else + else if (Verb_MeleeAttackCE.LastAttackVerb != null) { - if (Verb_MeleeAttackCE.LastAttackVerb != null) - { - localPenAmount = Verb_MeleeAttackCE.LastAttackVerb.ArmorPenetrationBlunt; - } - else - { - //LastAttackVerb is already checked in GetAfterArmorDamage(). Only known case of code arriving here is with the ancient soldiers - //spawned at the start of the game: their wounds are usually applied with Weapon==null and Instigator==null, so they skip CE's armor system, - //but on rare occasions, one of the soldiers gets Bite injuries with with Weapon==null and the instigator set as *himself*. - //Warning message below to identify any other situations where this might be happening. -LX7 - Log.Warning($"[CE] Deflection for Instigator:{dinfo.Instigator} Target:{dinfo.IntendedTarget} DamageDef:{dinfo.Def} Weapon:{dinfo.Weapon} has null verb, overriding AP."); - localPenAmount = 50; - } + penAmount = Verb_MeleeAttackCE.LastAttackVerb.ArmorPenetrationBlunt; } - localDmgAmount = Mathf.Pow(localPenAmount * 10000, 1 / 3f) / 10; - - //Fragment damage from large fragments often splits up into multiple attacks with reduced damage, which have the same sharp pen I believe, - //therefore the deflected damage should also be with reduced damage, but the same blunt pen. - //The damage info's damage amount isn't set anywhere within this class, therefore it is a good gauge on how many times the damage split. - if (dinfo.Weapon?.projectile is ProjectilePropertiesCE) + else { - localDmgAmount *= dinfo.Amount / (float)dinfo.Weapon.projectile.damageAmountBase; + Log.Warning($"[CE] Deflection for Instigator:{dinfo.Instigator} " + + $"Target:{dinfo.IntendedTarget} DamageDef:{dinfo.Def} " + + $"Weapon:{dinfo.Weapon} has null verb, overriding AP"); + penAmount = 50f; } + float dmgAmount = Mathf.Pow(penAmount * 10000f, 1f / 3f) / 10f * dmgMulti; - var newDinfo = new DamageInfo(DamageDefOf.Blunt, - localDmgAmount, - localPenAmount, - dinfo.Angle, - dinfo.Instigator, - GetOuterMostParent(hitPart), - partialPen ? null : dinfo.Weapon, //To not apply the secondary damage twice on partial penetrations. - instigatorGuilty: dinfo.InstigatorGuilty); - newDinfo.SetBodyRegion(dinfo.Height, dinfo.Depth); + newDinfo = new DamageInfo(DamageDefOf.Blunt, + dmgAmount, + penAmount, + dinfo.Angle, + dinfo.Instigator, + GetOuterMostParent(dinfo.HitPart), + dinfo.Weapon, + instigatorGuilty: dinfo.InstigatorGuilty); newDinfo.SetWeaponBodyPartGroup(dinfo.WeaponBodyPartGroup); newDinfo.SetWeaponHediff(dinfo.WeaponLinkedHediff); newDinfo.SetInstantPermanentInjury(dinfo.InstantPermanentInjury); newDinfo.SetAllowDamagePropagation(dinfo.AllowDamagePropagation); - if (!partialPen) //If it was a deflect, update dmgAmount and penAmount. + + return newDinfo; + } + + /// + /// Checks if a shield covers the hit part from an attack + /// + /// True if the shield does cover the body part, false otherwise + private static bool DoesShieldCover(BodyPartRecord hitPart, Pawn pawn, Apparel shield) + { + var shieldDef = shield.def.GetModExtension(); + if (shieldDef == null) { - dmgAmount = localDmgAmount; - penAmount = localPenAmount; + Log.ErrorOnce($"[CE] {shield.def} is Apparel_Shield but lacks ShieldDefExtension", + shield.def.GetHashCode() + 12748102); + return false; } - return newDinfo; + if (!shieldDef.PartIsCoveredByShield(hitPart, pawn.IsCrouching())) + { + return false; + } + // Right arm is vulnerable during warmup/attack/cooldown + return !hitPart.IsInGroup(CE_BodyPartGroupDefOf.RightArm) + || (pawn.stances?.curStance as Stance_Busy)?.verb == null; } /// - /// Retrieves the first parent of a body part with depth Outside + /// Gets the outer most parent of a supplied body part. /// - /// The part to get the parent of - /// The first parent part with depth Outside, the original part if it already is Outside or doesn't have a parent, the root part if no parents are Outside private static BodyPartRecord GetOuterMostParent(BodyPartRecord part) { var curPart = part; @@ -571,70 +671,152 @@ private static BodyPartRecord GetOuterMostParent(BodyPartRecord part) } /// - /// Determines whether a dinfo is of an ambient (i.e. heat, electric) damage type and should apply percentage reduction, as opposed to deflection-based reduction + /// Helper function to quickly get the armorRatingStat of a damage info /// - /// - /// True if dinfo armor category is Heat or Electric, false otherwise - private static bool IsAmbientDamage(this DamageInfo dinfo) + private static StatDef ArmorRatingStat(this ref DamageInfo dinfo) { - return (dinfo.Def.GetModExtension() ?? new DamageDefExtensionCE()).isAmbientDamage; + return dinfo.Def.armorCategory.armorRatingStat; } /// - /// Applies damage to a parry object based on its armor values. For ambient damage, percentage reduction is applied, direct damage uses deflection formulas. + /// Applies damage to a parry object based on its armor values. For ambient damage, + /// percentage reduction is applied, direct damage uses deflection formulas. /// /// DamageInfo to apply to parryThing /// Thing taking the damage - public static void ApplyParryDamage(DamageInfo dinfo, Thing parryThing) + public static void ApplyParryDamage(DamageInfo origDinfo, Thing parryThing) { - var pawn = parryThing as Pawn; - if (pawn != null) + var dinfo = new DamageInfo(origDinfo); + + if (parryThing is Pawn pawn) { // Pawns run their own armor calculations - dinfo.SetAmount(dinfo.Amount * Mathf.Clamp01(Rand.Range(0.5f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance), 1f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance) * 1.25f))); + dinfo.SetAmount(dinfo.Amount * Mathf.Clamp01(Rand.Range( + 0.5f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance), + 1f - pawn.GetStatValue(CE_StatDefOf.MeleeParryChance) * 1.25f))); pawn.TakeDamage(dinfo); + return; } - else if (dinfo.IsAmbientDamage()) + + if (dinfo.IsAmbientDamage()) { - var dmgAmount = Mathf.CeilToInt(dinfo.Amount * Mathf.Clamp01(parryThing.GetStatValue(dinfo.Def.armorCategory.armorRatingStat))); - dinfo.SetAmount(dmgAmount); + dinfo.SetAmount(Mathf.CeilToInt(dinfo.Amount * Mathf.Clamp01( + parryThing.GetStatValue(dinfo.ArmorRatingStat())))); parryThing.TakeDamage(dinfo); + return; } - else + + dinfo.SetAmount(dinfo.Amount * PARRY_THING_DAMAGE_FACTOR); + // Initialize the AttackInfo linked list + var ainfo = new AttackInfo(dinfo); + if (dinfo.ArmorRatingStat() == StatDefOf.ArmorRating_Sharp) { - float parryThingArmor; - var dmgAmount = dinfo.Amount * 0.5f; + var bluntDinfo = GetDeflectDamageInfo(dinfo); + bluntDinfo.SetAmount(bluntDinfo.Amount * PARRY_THING_DAMAGE_FACTOR); + ainfo.Append(bluntDinfo); + } - // For apparel - if (parryThing.def.IsApparel) + // Shield was used to parry + if (parryThing is Apparel app) + { + for (var info = ainfo; + info != null && app != null && !app.Destroyed; + info = info.next) { - parryThingArmor = parryThing.GetStatValue(dinfo.Def.armorCategory.armorRatingStat); + var blockedPenAmount = PenetrateArmor(info, app); + // Blocked penetration amount decays into next attack penetration + if (info.next != null) + { + info.next.penAmount += info.penTransferRate * blockedPenAmount; + } } - // Special case for weapons - else + return; + } + + // Weapon was used to parry + if (parryThing.def.IsWeapon) + { + for (var info = ainfo; + info != null && parryThing != null && !parryThing.Destroyed; + info = info.next) { - parryThingArmor = parryThing.GetStatValue(CE_StatDefOf.ToughnessRating); - // Compensation for blunt damage against weapons - if (dinfo.Def.armorCategory != DamageArmorCategoryDefOf.Sharp) + // Blocked penetration amount decays into next attack penetration + var blockedPenAmount = PenetrateWeapon(info, parryThing); + if (info.next != null) { - parryThingArmor *= 1.5f; + info.next.penAmount += info.penTransferRate * blockedPenAmount; } } + return; + } + } - var penAmount = dinfo.ArmorPenetrationInt; //GetPenetrationValue(dinfo); + /// + /// Determines whether a dinfo is of an ambient (i.e. heat, electric) damage type + /// and should apply percentage reduction, as opposed to deflection-based reduction. + /// + /// + /// True if dinfo armor category is Heat or Electric, false otherwise + private static bool IsAmbientDamage(this DamageInfo dinfo) + { + return (dinfo.Def.GetModExtension() + ?? new DamageDefExtensionCE()).isAmbientDamage; + } - bool partialPen = TryPenetrateArmor(dinfo.Def, parryThingArmor, ref penAmount, ref dmgAmount, parryThing); + /// + /// Calculates damage reduction for ambient damage types (fire, electricity) versus natural + /// and worn armor of a pawn. Adds up the total armor percentage (clamped at 0-100%) and + /// multiplies damage by that amount. + /// + /// The original amount of damage + /// The armor stat to use for damage reduction + /// The damaged pawn + /// The body part affected + /// The post-armor damage ranging from 0 to the original amount + private static float GetAmbientPostArmorDamage(float dmgAmount, StatDef armorRatingStat, + Pawn pawn, BodyPartRecord part) + { + var dmgMult = 1f; + if (part.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor)) + { + dmgMult -= pawn.PartialStat(armorRatingStat, part); + } - if (dinfo.Def.armorCategory == DamageArmorCategoryDefOf.Sharp && dmgAmount > 0) + if (dmgMult <= 0) + { + return 0; + } + if (pawn.apparel != null && !pawn.apparel.WornApparel.NullOrEmpty()) + { + var apparelList = pawn.apparel.WornApparel; + foreach (var apparel in apparelList) { - var ndi = GetDeflectDamageInfo(dinfo, dinfo.HitPart, ref dmgAmount, ref penAmount, partialPen); - if (dmgAmount > 0) + if (apparel.def.apparel.CoversBodyPart(part)) + { + dmgMult -= apparel.PartialStat(armorRatingStat, part); + } + if (dmgMult <= 0) { - ApplyParryDamage(ndi, parryThing); + dmgMult = 0; + break; } } + } + var deflectionComp = pawn.TryGetComp(); + if (deflectionComp != null) + { + if (armorRatingStat == StatDefOf.ArmorRating_Heat) + { + if (deflectionComp.deflectedSharp) + { + dmgMult /= 2f; + } + } + deflectionComp.deflectedSharp = false; } + + return (float)Math.Floor(dmgAmount * dmgMult); } #endregion diff --git a/Source/CombatExtended/CombatExtended/CE_Utility.cs b/Source/CombatExtended/CombatExtended/CE_Utility.cs index 1824ab98ef..c3db9a8906 100644 --- a/Source/CombatExtended/CombatExtended/CE_Utility.cs +++ b/Source/CombatExtended/CombatExtended/CE_Utility.cs @@ -439,66 +439,88 @@ public static T GetLastModExtension(this Def def) where T : DefModExtension /// public static float PartialStat(this Apparel apparel, StatDef stat, BodyPartRecord part) { + float result = apparel.GetStatValue(stat); + if (part == null) + { + return result; + } + if (!apparel.def.apparel.CoversBodyPart(part)) { - return 0; + var shieldDef = apparel.def.GetModExtension(); + if (shieldDef == null || !shieldDef.PartIsCoveredByShield(part, true)) + { + return 0f; + } } - float result = apparel.GetStatValue(stat); + if (!Controller.settings.PartialStat) + { + return result; + } - if (Controller.settings.PartialStat) + PartialArmorExt partialExt = apparel.def.GetModExtension(); + if (partialExt == null) { - if (apparel.def.HasModExtension()) + return result; + } + foreach (ApparelPartialStat partial in partialExt.stats) + { + if (partial.stat != stat || (!partial.parts?.Contains(part.def) ?? true)) { - foreach (ApparelPartialStat partial in apparel.def.GetModExtension().stats) - { - if ((partial?.parts?.Contains(part.def) ?? false) | ((partial?.parts?.Contains(part?.parent?.def) ?? false) && part.depth == BodyPartDepth.Inside)) - { - - if (partial.staticValue > 0f) - { - return partial.staticValue; - } - result *= partial.mult; - break; + continue; + } - } - } + if (partial.staticValue > 0f) + { + return partial.staticValue; } + return result * partial.mult; } + return result; } /// /// Gets the true rating of armor with partial stats taken into account /// - public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartRecord part, float damage = 0f, float AP = 0f) + public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartRecord part) { float result = pawn.GetStatValue(stat); + if (part == null) + { + return result; + } - if (Controller.settings.PartialStat) + if (!part.IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor)) { - if (pawn.def.HasModExtension()) - { - foreach (ApparelPartialStat partial in pawn.def.GetModExtension().stats) - { - if (partial.stat == stat) - { - if ((partial?.parts?.Contains(part.def) ?? false) | ((partial?.parts?.Contains(part?.parent?.def) ?? false) && part.depth == BodyPartDepth.Inside)) - { + return 0f; + } - if (partial.staticValue > 0f) - { - return partial.staticValue; - } - result *= partial.mult; - break; + if (!Controller.settings.PartialStat) + { + return result; + } - } - } - } + var partialExt = pawn.def.GetModExtension(); + if (partialExt == null) + { + return result; + } + foreach (ApparelPartialStat partial in partialExt.stats) + { + if (partial.stat != stat || (!partial.parts?.Contains(part.def) ?? true)) + { + continue; } + + if (partial.staticValue > 0f) + { + return partial.staticValue; + } + return result * partial.mult; } + return result; } @@ -508,23 +530,26 @@ public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartRecord par public static float PartialStat(this Apparel apparel, StatDef stat, BodyPartDef part) { float result = apparel.GetStatValue(stat); - if (apparel.def.HasModExtension()) + + var partialExt = apparel.def.GetModExtension(); + if (partialExt == null) { - foreach (ApparelPartialStat partial in apparel.def.GetModExtension().stats) + return result; + } + foreach (ApparelPartialStat partial in partialExt.stats) + { + if (partial.stat != stat || (!partial.parts?.Contains(part) ?? true)) { - if ((partial?.parts?.Contains(part) ?? false)) - { - - if (partial.staticValue > 0f) - { - return partial.staticValue; - } - result *= partial.mult; - break; + continue; + } - } + if (partial.staticValue > 0f) + { + return partial.staticValue; } + return result * partial.mult; } + return result; } @@ -534,21 +559,26 @@ public static float PartialStat(this Apparel apparel, StatDef stat, BodyPartDef public static float PartialStat(this Pawn pawn, StatDef stat, BodyPartDef part) { float result = pawn.GetStatValue(stat); - if (pawn.def.HasModExtension()) + + if (!pawn.health.hediffSet.GetBodyPartRecord(part) + .IsInGroup(CE_BodyPartGroupDefOf.CoveredByNaturalArmor)) + { + return 0f; + } + + var partialExt = pawn.def.GetModExtension(); + foreach (ApparelPartialStat partial in partialExt.stats) { - foreach (ApparelPartialStat partial in pawn.def.GetModExtension().stats) + if (partial.stat != stat || (!partial.parts?.Contains(part) ?? true)) { - if ((partial?.parts?.Contains(part) ?? false)) - { - if (partial.staticValue > 0f) - { - return partial.staticValue; - } - result *= partial.mult; - break; + continue; + } - } + if (partial.staticValue > 0f) + { + return partial.staticValue; } + return result * partial.mult; } return result; } diff --git a/Source/CombatExtended/CombatExtended/Defs/ShieldDefExtension.cs b/Source/CombatExtended/CombatExtended/Defs/ShieldDefExtension.cs index 0e22f414f8..422d7f834b 100644 --- a/Source/CombatExtended/CombatExtended/Defs/ShieldDefExtension.cs +++ b/Source/CombatExtended/CombatExtended/Defs/ShieldDefExtension.cs @@ -14,7 +14,7 @@ public class ShieldDefExtension : DefModExtension public List crouchCoverage = new List(); public bool drawAsTall = false; - public bool PartIsCoveredByShield(BodyPartRecord part, Pawn pawn) + public bool PartIsCoveredByShield(BodyPartRecord part, bool isCrouching) { if (!shieldCoverage.NullOrEmpty()) { @@ -26,7 +26,7 @@ public bool PartIsCoveredByShield(BodyPartRecord part, Pawn pawn) } } } - if (!crouchCoverage.NullOrEmpty() && pawn.IsCrouching()) + if (!crouchCoverage.NullOrEmpty() && isCrouching) { foreach (BodyPartGroupDef group in crouchCoverage) { diff --git a/Source/CombatExtended/Harmony/Harmony_DamageWorker_AddInjury.cs b/Source/CombatExtended/Harmony/Harmony_DamageWorker_AddInjury.cs index 0a254625c4..a34235dbb8 100644 --- a/Source/CombatExtended/Harmony/Harmony_DamageWorker_AddInjury.cs +++ b/Source/CombatExtended/Harmony/Harmony_DamageWorker_AddInjury.cs @@ -14,21 +14,29 @@ namespace CombatExtended.HarmonyCE [HarmonyPatch(typeof(DamageWorker_AddInjury), "ApplyDamageToPart")] internal static class Harmony_DamageWorker_AddInjury_ApplyDamageToPart { + // Set to false initially and in postfix, may be set to true in GetAfterArmorDamage + private static bool _skipSecondary = false; + // Secondary damage debounce boolean private static bool _applyingSecondary = false; - private static bool shieldAbsorbed = false; - private static readonly int[] ArmorBlockNullOps = { 1, 3, 4, 5, 6 }; // Lines in armor block that need to be nulled out + // Lines in armor block that need to be nulled out + private static readonly int[] ArmorBlockNullOps = { 1, 3, 4, 5, 6 }; - private static void ArmorReroute(Pawn pawn, ref DamageInfo dinfo, out bool deflectedByArmor, out bool diminishedByArmor) + private static void ArmorReroute(Pawn pawn, ref DamageInfo dinfo, + out bool deflectedByArmor, out bool diminishedByArmor) { - var newDinfo = ArmorUtilityCE.GetAfterArmorDamage(dinfo, pawn, dinfo.HitPart, out deflectedByArmor, out diminishedByArmor, out shieldAbsorbed); + var newDinfo = ArmorUtilityCE.GetAfterArmorDamage(dinfo, pawn, + out deflectedByArmor, out diminishedByArmor, out _skipSecondary); if (dinfo.HitPart != newDinfo.HitPart) { if (pawn.Spawned) { - LessonAutoActivator.TeachOpportunity(CE_ConceptDefOf.CE_ArmorSystem, OpportunityType.Critical); // Inform the player about armor deflection + // Inform the player about armor deflection + LessonAutoActivator.TeachOpportunity(CE_ConceptDefOf.CE_ArmorSystem, + OpportunityType.Critical); } } - Patch_CheckDuplicateDamageToOuterParts.lastHitPartHealth = pawn.health.hediffSet.GetPartHealth(newDinfo.HitPart); + Patch_CheckDuplicateDamageToOuterParts.lastHitPartHealth = + pawn.health.hediffSet.GetPartHealth(newDinfo.HitPart); dinfo = newDinfo; } @@ -39,7 +47,9 @@ internal static IEnumerable Transpiler(IEnumerable ReferenceEquals(c.operand, typeof(ArmorUtility).GetMethod("GetPostArmorDamage", AccessTools.all))); + var armorBlockEnd = codes.FirstIndexOf( + c => ReferenceEquals(c.operand, + typeof(ArmorUtility).GetMethod("GetPostArmorDamage", AccessTools.all))); int armorBlockStart = -1; @@ -54,7 +64,8 @@ internal static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable