/// <summary> /// returns true if 10% armor damage was incurred or any structure damage /// </summary> /// <param name="attackSequence"></param> /// <returns></returns> private static bool SufficientDamageWasDone(AttackDirector.AttackSequence attackSequence) { if (attackSequence == null) { return(false); } var id = attackSequence.chosenTarget.GUID; if (!attackSequence.GetAttackDidDamage(id)) { LogDebug("No damage"); return(false); } var previousArmor = Patches.mechArmorBeforeAttack; var previousStructure = Patches.mechStructureBeforeAttack; LogDebug($"Damage >>> A: {attackSequence.GetArmorDamageDealt(id):#.###}" + $" S: {attackSequence.GetStructureDamageDealt(id):#.###}" + $" ({(attackSequence.GetArmorDamageDealt(id) + attackSequence.GetStructureDamageDealt(id)) / (previousArmor + previousStructure) * 100:#.##}%) H: {Patches.heatDamage}"); if (attackSequence.GetStructureDamageDealt(id) >= modSettings.MinimumStructureDamageRequired) { LogDebug("Structure damage requires panic save"); return(true); } float heatTaken = 0; if (attackSequence.chosenTarget is Mech defender) { heatTaken = defender.CurrentHeat - Patches.mechHeatBeforeAttack; // LogDebug($"B {Patches.mechHeatBeforeAttack} A {defender.CurrentHeat}"); LogDebug($"Took {Patches.heatDamage} heat"); } LogDebug($"attackSequence.GetArmorDamageDealt(id) {attackSequence.GetArmorDamageDealt(id)}\nattackSequence.GetStructureDamageDealt(id) {attackSequence.GetStructureDamageDealt(id)}\nPatches.heatDamage * modSettings.HeatDamageModifier {Patches.heatDamage * modSettings.HeatDamageModifier}"); if (attackSequence.GetArmorDamageDealt(id) + attackSequence.GetStructureDamageDealt(id) + Patches.heatDamage * modSettings.HeatDamageModifier / (previousArmor + previousStructure) + 100 <= modSettings.MinimumDamagePercentageRequired) { LogDebug("Not enough damage"); return(false); } LogDebug("Total damage requires a panic save"); return(true); }
// returns true if enough damage was inflicted to trigger a panic save private static bool SufficientDamageWasDone(AttackDirector.AttackSequence attackSequence) { if (attackSequence == null) { return(false); } var id = attackSequence.chosenTarget.GUID; if (!attackSequence.GetAttackDidDamage(id)) { LogReport("No damage"); return(false); } var previousArmor = AttackStackSequence_OnAttackBegin_Patch.armorBeforeAttack; var previousStructure = AttackStackSequence_OnAttackBegin_Patch.structureBeforeAttack; var armorDamage = attackSequence.GetArmorDamageDealt(id); var structureDamage = attackSequence.GetStructureDamageDealt(id); var percentDamageDone = (attackSequence.GetArmorDamageDealt(id) + attackSequence.GetStructureDamageDealt(id)) / (previousArmor + previousStructure) * 100; damageWithHeatDamage = percentDamageDone + Mech_AddExternalHeat_Patch.heatDamage * modSettings.HeatDamageFactor; // have to check structure here AFTER armor, despite it being the priority, because we need to set the global LogReport($"Damage >>> A: {armorDamage:F3} S: {structureDamage:F3} ({percentDamageDone:F2}%) H: {Mech_AddExternalHeat_Patch.heatDamage}"); if (attackSequence.chosenTarget is Mech && attackSequence.GetStructureDamageDealt(id) >= modSettings.MinimumMechStructureDamageRequired || modSettings.VehiclesCanPanic && attackSequence.chosenTarget is Vehicle && attackSequence.GetStructureDamageDealt(id) >= modSettings.MinimumVehicleStructureDamageRequired) { LogReport("Structure damage requires panic save"); return(true); } if (damageWithHeatDamage <= modSettings.MinimumDamagePercentageRequired) { LogReport("Not enough damage"); Mech_AddExternalHeat_Patch.heatDamage = 0; return(false); } LogReport("Total damage requires a panic save"); return(true); }
// returns true if enough damage was inflicted to trigger a panic save private static bool SufficientDamageWasDone(AttackDirector.AttackSequence attackSequence) { if (attackSequence == null) { return(false); } var id = attackSequence.chosenTarget.GUID; if (!attackSequence.GetAttackDidDamage(id)) { LogReport("No damage"); return(false); } // Account for melee attacks so separate panics are not triggered. if (attackSequence.isMelee && MechMeleeSequence_FireWeapons_Patch.meleeHasSupportWeapons) { initialArmorMelee = AttackStackSequence_OnAttackBegin_Patch.armorBeforeAttack; initialStructureMelee = AttackStackSequence_OnAttackBegin_Patch.structureBeforeAttack; armorDamageMelee = attackSequence.GetArmorDamageDealt(id); structureDamageMelee = attackSequence.GetStructureDamageDealt(id); hadMeleeAttack = true; LogReport("Stashing melee damage for support weapon firing"); return(false); } var previousArmor = AttackStackSequence_OnAttackBegin_Patch.armorBeforeAttack; var previousStructure = AttackStackSequence_OnAttackBegin_Patch.structureBeforeAttack; if (hadMeleeAttack) { LogReport("Adding stashed melee damage"); previousArmor = initialArmorMelee; previousStructure = initialStructureMelee; } else { armorDamageMelee = 0; structureDamageMelee = 0; } var armorDamage = attackSequence.GetArmorDamageDealt(id) + armorDamageMelee; var structureDamage = attackSequence.GetStructureDamageDealt(id) + structureDamageMelee; var heatDamage = Mech_AddExternalHeat_Patch.heatDamage * modSettings.HeatDamageFactor; // used in SavingThrows.cs damageIncludingHeatDamage = armorDamage + structureDamage + heatDamage; var percentDamageDone = damageIncludingHeatDamage / (previousArmor + previousStructure) * 100; // clear melee values initialArmorMelee = 0; initialStructureMelee = 0; armorDamageMelee = 0; structureDamageMelee = 0; hadMeleeAttack = false; // have to check structure here AFTER armor, despite it being the priority, because we need to set the global LogReport($"Damage >>> A: {armorDamage:F3} S: {structureDamage:F3} ({percentDamageDone:F2}%) H: {Mech_AddExternalHeat_Patch.heatDamage}"); if (modSettings.AlwaysPanic) { LogReport("AlwaysPanic"); return(true); } if (attackSequence.chosenTarget is Mech && attackSequence.GetStructureDamageDealt(id) > modSettings.MinimumMechStructureDamageRequired || modSettings.VehiclesCanPanic && attackSequence.chosenTarget is Vehicle && attackSequence.GetStructureDamageDealt(id) > modSettings.MinimumVehicleStructureDamageRequired) { LogReport("Structure damage requires panic save"); return(true); } if (percentDamageDone <= modSettings.MinimumDamagePercentageRequired) { LogReport("Not enough damage"); Mech_AddExternalHeat_Patch.heatDamage = 0; return(false); } LogReport("Total damage requires a panic save"); return(true); }
static void Postfix(AbstractActor __instance, string sourceID, int sequenceID, int stackItemID, AttackDirection attackDirection) { try { AttackDirector.AttackSequence attackSequence = __instance.Combat.AttackDirector.GetAttackSequence(sequenceID); if (attackSequence != null && attackSequence.GetAttackDidDamage(__instance.GUID)) { List <Effect> list = __instance.Combat.EffectManager.GetAllEffectsTargeting(__instance).FindAll((Effect x) => x.EffectData.targetingData.effectTriggerType == EffectTriggerType.OnDamaged); for (int i = 0; i < list.Count; i++) { list[i].OnEffectTakeDamage(attackSequence.attacker, __instance); } if (attackSequence.isMelee) { int value = attackSequence.attacker.StatCollection.GetValue <int>("MeleeHitPushBackPhases"); if (value > 0) { for (int j = 0; j < value; j++) { __instance.ForceUnitOnePhaseDown(sourceID, stackItemID, false); } } } } int evasivePipsCurrent = __instance.EvasivePipsCurrent; var settings = PermanentEvasion.Settings; float totalDamageReceived = 1; if (attackSequence.GetAttackDidDamage(__instance.GUID)) { totalDamageReceived += attackSequence.GetArmorDamageDealt(__instance.GUID) + attackSequence.GetStructureDamageDealt(__instance.GUID); if ((totalDamageReceived > settings.MinDamageForEvasionStrip) && settings.AllowHitStrip) { __instance.ConsumeEvasivePip(true); Fields.LoosePip = false; } else if (Fields.LoosePip) { __instance.ConsumeEvasivePip(true); Fields.LoosePip = false; } } else if (Fields.LoosePip) { __instance.ConsumeEvasivePip(true); Fields.LoosePip = false; } int evasivePipsCurrent2 = __instance.EvasivePipsCurrent; if (evasivePipsCurrent2 < evasivePipsCurrent && (totalDamageReceived > settings.MinDamageForEvasionStrip) && settings.AllowHitStrip && !__instance.IsDead && !__instance.IsFlaggedForDeath) { __instance.Combat.MessageCenter.PublishMessage(new FloatieMessage(__instance.GUID, __instance.GUID, "HIT: -1 EVASION", FloatieMessage.MessageNature.Debuff)); } else if (evasivePipsCurrent2 < evasivePipsCurrent && !__instance.IsDead && !__instance.IsFlaggedForDeath) { __instance.Combat.MessageCenter.PublishMessage(new FloatieMessage(__instance.GUID, __instance.GUID, "-1 EVASION", FloatieMessage.MessageNature.Debuff)); } else if (evasivePipsCurrent2 > 0 && Fields.KeptPip && !__instance.IsDead && !__instance.IsFlaggedForDeath) { __instance.Combat.MessageCenter.PublishMessage(new FloatieMessage(__instance.GUID, __instance.GUID, "EVASION KEPT", FloatieMessage.MessageNature.Buff)); Fields.KeptPip = false; } } catch (Exception e) { Logger.Error(e); } }
public static bool ShouldPanic(Mech mech, AttackDirector.AttackSequence attackSequence) { if (mech == null || attackSequence == null) { return(false); } if (mech.IsDead || (mech.IsFlaggedForDeath && mech.HasHandledDeath) || mech.team == null) { return(false); } var id = attackSequence.chosenTarget.GUID; if (!attackSequence.GetAttackDidDamage(id)) //no point in panicking over nothing { return(false); } if (attackSequence.GetStructureDamageDealt(id) < 1 && !attackSequence.GetLowArmorStruck(id)) //no structure damage and didn't strike low armour { float totalArmor = 0, maxArmor = 0; maxArmor = GetTotalMechArmour(mech, maxArmor); totalArmor = GetCurrentMechArmour(mech, totalArmor); if ((totalArmor / maxArmor * 100) + ((BasicPanic.Settings.MinimumArmourDamagePercentageRequired * maxArmor / 100) / maxArmor * 100) >= 100) //basically if this equals to 100%, mech didn't lose enough armour { return(false); } } if (mech.team == mech.Combat.LocalPlayerTeam && !BasicPanic.Settings.PlayerTeamCanPanic) { return(false); } else if (mech.team != mech.Combat.LocalPlayerTeam && !BasicPanic.Settings.EnemiesCanPanic) { return(false); } int PanicRoll = 0; Pilot pilot = mech.GetPilot(); var weapons = mech.Weapons; var guts = mech.SkillGuts; var tactics = mech.SkillTactics; var total = guts + tactics; int index = -1; index = PanicHelpers.GetTrackedPilotIndex(mech); float lowestRemaining = mech.CenterTorsoStructure + mech.CenterTorsoFrontArmor; float panicModifiers = 0; if (index < 0) { Holder.TrackedPilots.Add(new PanicTracker(mech)); //add a new tracker to tracked pilot, then we run it all over again; index = PanicHelpers.GetTrackedPilotIndex(mech); if (index < 0) { return(false); } } if (Holder.TrackedPilots[index].trackedMech != mech.GUID) { return(false); } if (Holder.TrackedPilots[index].trackedMech == mech.GUID && Holder.TrackedPilots[index].ChangedRecently && BasicPanic.Settings.AlwaysGatedChanges) { return(false); } // pilot health if (pilot != null) { float pilotHealthPercent = 1 - ((float)pilot.Injuries / pilot.Health); if (pilotHealthPercent < 1) { panicModifiers += BasicPanic.Settings.PilotHealthMaxModifier * (1 - pilotHealthPercent); } } if (mech.IsUnsteady) { panicModifiers += BasicPanic.Settings.UnsteadyModifier; } // Head var headHealthPercent = (mech.HeadArmor + mech.HeadStructure) / (mech.GetMaxArmor(ArmorLocation.Head) + mech.GetMaxStructure(ChassisLocations.Head)); if (headHealthPercent < 1) { panicModifiers += BasicPanic.Settings.HeadDamageMaxModifier * (1 - headHealthPercent); } // CT var ctPercent = (mech.CenterTorsoFrontArmor + mech.CenterTorsoStructure) / (mech.GetMaxArmor(ArmorLocation.CenterTorso) + mech.GetMaxStructure(ChassisLocations.CenterTorso)); if (ctPercent < 1) { panicModifiers += BasicPanic.Settings.CTDamageMaxModifier * (1 - ctPercent); lowestRemaining = Math.Min(mech.CenterTorsoStructure, lowestRemaining); } // side torsos var ltStructurePercent = mech.LeftTorsoStructure / mech.GetMaxStructure(ChassisLocations.LeftTorso); if (ltStructurePercent < 1) { panicModifiers += BasicPanic.Settings.SideTorsoInternalDamageMaxModifier * (1 - ltStructurePercent); } var rtStructurePercent = mech.RightTorsoStructure / mech.GetMaxStructure(ChassisLocations.RightTorso); if (rtStructurePercent < 1) { panicModifiers += BasicPanic.Settings.SideTorsoInternalDamageMaxModifier * (1 - rtStructurePercent); } // legs if (mech.RightLegDamageLevel == LocationDamageLevel.Destroyed || mech.LeftLegDamageLevel == LocationDamageLevel.Destroyed) { float legPercent; if (mech.LeftLegDamageLevel == LocationDamageLevel.Destroyed) { legPercent = (mech.RightLegStructure + mech.RightLegArmor) / (mech.GetMaxStructure(ChassisLocations.RightLeg) + mech.GetMaxArmor(ArmorLocation.RightLeg)); } else { legPercent = (mech.LeftLegStructure + mech.LeftLegArmor) / (mech.GetMaxStructure(ChassisLocations.LeftLeg) + mech.GetMaxArmor(ArmorLocation.LeftLeg)); } if (legPercent < 1) { lowestRemaining = Math.Min(legPercent * (mech.GetMaxStructure(ChassisLocations.LeftLeg) + mech.GetMaxArmor(ArmorLocation.LeftLeg)), lowestRemaining); panicModifiers += BasicPanic.Settings.LeggedMaxModifier * (1 - legPercent); } } // next shot could kill if (lowestRemaining <= attackSequence.cumulativeDamage) { panicModifiers += BasicPanic.Settings.NextShotLikeThatCouldKill; } // weaponless if (weapons.TrueForAll(w => w.DamageLevel == ComponentDamageLevel.Destroyed || w.DamageLevel == ComponentDamageLevel.NonFunctional)) { panicModifiers += BasicPanic.Settings.WeaponlessModifier; } // alone if (mech.Combat.GetAllAlliesOf(mech).TrueForAll(m => m.IsDead || m == mech as AbstractActor)) { panicModifiers += BasicPanic.Settings.AloneModifier; } //straight up add guts, tactics, and morale to this as negative values panicModifiers -= total; if (mech.team == mech.Combat.LocalPlayerTeam) { MoraleConstantsDef moraleDef = mech.Combat.Constants.GetActiveMoraleDef(mech.Combat); panicModifiers -= Math.Max(mech.Combat.LocalPlayerTeam.Morale - moraleDef.CanUseInspireLevel, 0) / (float)2; } //reduce modifiers by 5 to account change to D20 roll instead of D100 roll, then min it t0 20 or modified floor panicModifiers /= 5; PanicRoll = PanicRoll + (int)panicModifiers; if ((total >= 20 || PanicRoll <= 0) && !BasicPanic.Settings.AtLeastOneChanceToPanic) { return(false); } PanicRoll = Math.Min(PanicRoll, 20); if (PanicRoll < 0) { PanicRoll = 0; //make this have some kind of chance to happen } PanicRoll = UnityEngine.Random.Range(PanicRoll, 20); // actual roll //we get this far, we reduce total to under the max panic chance total = Math.Min(total, (int)BasicPanic.Settings.MaxPanicResistTotal); int rngRoll = UnityEngine.Random.Range(total, 20); if (rngRoll <= PanicRoll) { ApplyPanicDebuff(mech, index); return(true); } mech.Combat.MessageCenter.PublishMessage(new AddSequenceToStackMessage(new ShowActorInfoSequence(mech, $"Resisted Morale Check!", FloatieMessage.MessageNature.Buff, true))); return(false); }
public static void Postfix(AttackDirector __instance, MessageCenterMessage message) { Mod.Log.Debug?.Write("AD:OASE - entered."); // Nothing to do if (ModState.BreachAttackId == ModState.NO_ATTACK_SEQUENCE_ID) { return; } AttackSequenceEndMessage attackSequenceEndMessage = message as AttackSequenceEndMessage; if (__instance == null || attackSequenceEndMessage == null) { return; } int sequenceId = attackSequenceEndMessage.sequenceId; AttackDirector.AttackSequence attackSequence = __instance.GetAttackSequence(sequenceId); if (attackSequence == null) { Mod.Log.Info?.Write($"Could not find attack sequence with id: {sequenceId}. CAC may have killed it, or there's an error in processing. Skipping hull breach checks."); return; } // No chosen target, nothing to damage if (attackSequence.chosenTarget == null) { return; } if (ModState.BreachAttackId != attackSequence.id) { Mod.Log.Debug?.Write($"Attack sequence ID {attackSequence.id} does not match Hull Breach Attack Id - skipping hull breach resolution."); return; } float structureDamage = attackSequence.GetStructureDamageDealt(attackSequence.chosenTarget.GUID); Mod.Log.Debug?.Write($"Attack sequence {sequenceId} did {structureDamage} structure damage to target: {attackSequence.chosenTarget.GUID}"); if (structureDamage == 0f) { Mod.Log.Debug?.Write($"Attack did no structure damage, skipping."); return; } if (attackSequence.chosenTarget is Mech targetMech) { Mod.Log.Debug?.Write($"Checking hull breaches for targetMech: {CombatantUtils.Label(targetMech)}"); ResolveMechHullBreaches(targetMech); } if (attackSequence.chosenTarget is Turret targetTurret) { Mod.Log.Debug?.Write($"Checking hull breaches for targetTurret: {CombatantUtils.Label(targetTurret)}"); ResolveTurretHullBreaches(targetTurret); } if (attackSequence.chosenTarget is Vehicle targetVehicle) { Mod.Log.Debug?.Write($"Checking hull breaches for targetVehicle: {CombatantUtils.Label(targetVehicle)}"); ResolveVehicleHullBreaches(targetVehicle); } // Reset state ModState.BreachAttackId = ModState.NO_ATTACK_SEQUENCE_ID; ModState.BreachHitsMech.Clear(); ModState.BreachHitsTurret.Clear(); ModState.BreachHitsVehicle.Clear(); Mod.Log.Debug?.Write("AD:OASE - exiting."); }