コード例 #1
0
        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);
            }

            if (!attackSequence.attackDidDamage) //no point in panicking over nothing
            {
                return(false);
            }

            if (!attackSequence.attackDamagedStructure && !attackSequence.lowArmorStruck) //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);
        }
コード例 #2
0
        public static bool RollForEjectionResult(Mech mech, AttackDirector.AttackSequence attackSequence, bool IsEarlyPanic)
        {
            if (mech == null || mech.IsDead || (mech.IsFlaggedForDeath && !mech.HasHandledDeath))
            {
                return(false);
            }

            // knocked down mechs cannot eject
            if (mech.IsProne && Settings.KnockedDownCannotEject)
            {
                return(false);
            }

            // have to do damage
            if (!attackSequence.attackDidDamage)
            {
                return(false);
            }

            Pilot pilot   = mech.GetPilot();
            var   weapons = mech.Weapons;
            var   guts    = mech.SkillGuts;
            var   tactics = mech.SkillTactics;
            var   total   = guts + tactics;

            float lowestRemaining = mech.CenterTorsoStructure + mech.CenterTorsoFrontArmor;
            float ejectModifiers  = 0;

            // guts 10 makes you immune, player character cannot be forced to eject
            if ((guts >= 10 && Settings.GutsTenAlwaysResists) || (pilot != null && pilot.IsPlayerCharacter && Settings.PlayerCharacterAlwaysResists))
            {
                return(false);
            }

            // tactics 10 makes you immune, or combination of guts and tactics makes you immune.
            if ((tactics >= 10 && Settings.TacticsTenAlwaysResists) || (total >= 10 && Settings.ComboTenAlwaysResists))
            {
                return(false);
            }

            // pilots that cannot eject or be headshot shouldn't eject
            if (!mech.CanBeHeadShot || (pilot != null && !pilot.CanEject))
            {
                return(false);
            }

            // pilot health
            if (pilot != null)
            {
                float pilotHealthPercent = 1 - ((float)pilot.Injuries / pilot.Health);

                if (pilotHealthPercent < 1)
                {
                    ejectModifiers += Settings.PilotHealthMaxModifier * (1 - pilotHealthPercent);
                }
            }

            if (mech.IsUnsteady)
            {
                ejectModifiers += Settings.UnsteadyModifier;
            }

            // Head
            var headHealthPercent = (mech.HeadArmor + mech.HeadStructure) / (mech.GetMaxArmor(ArmorLocation.Head) + mech.GetMaxStructure(ChassisLocations.Head));

            if (headHealthPercent < 1)
            {
                ejectModifiers += Settings.HeadDamageMaxModifier * (1 - headHealthPercent);
            }

            // CT
            var ctPercent = (mech.CenterTorsoFrontArmor + mech.CenterTorsoStructure) / (mech.GetMaxArmor(ArmorLocation.CenterTorso) + mech.GetMaxStructure(ChassisLocations.CenterTorso));

            if (ctPercent < 1)
            {
                ejectModifiers += Settings.CTDamageMaxModifier * (1 - ctPercent);
                lowestRemaining = Math.Min(mech.CenterTorsoStructure, lowestRemaining);
            }

            // side torsos
            var ltStructurePercent = mech.LeftTorsoStructure / mech.GetMaxStructure(ChassisLocations.LeftTorso);

            if (ltStructurePercent < 1)
            {
                ejectModifiers += Settings.SideTorsoInternalDamageMaxModifier * (1 - ltStructurePercent);
            }

            var rtStructurePercent = mech.RightTorsoStructure / mech.GetMaxStructure(ChassisLocations.RightTorso);

            if (rtStructurePercent < 1)
            {
                ejectModifiers += 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);
                    ejectModifiers += Settings.LeggedMaxModifier * (1 - legPercent);
                }
            }

            // next shot could kill
            if (lowestRemaining <= attackSequence.cumulativeDamage)
            {
                ejectModifiers += Settings.NextShotLikeThatCouldKill;
            }

            // weaponless
            if (weapons.TrueForAll(w =>
                                   w.DamageLevel == ComponentDamageLevel.Destroyed || w.DamageLevel == ComponentDamageLevel.NonFunctional))
            {
                ejectModifiers += Settings.WeaponlessModifier;
            }

            // alone
            if (mech.Combat.GetAllAlliesOf(mech).TrueForAll(m => m.IsDead || m == mech as AbstractActor))
            {
                ejectModifiers += Settings.AloneModifier;
            }

            var modifiers = (ejectModifiers - Settings.BaseEjectionResist - (Settings.GutsEjectionResistPerPoint * guts) - (Settings.TacticsEjectionResistPerPoint * tactics)) * 5;

            if (mech.team == mech.Combat.LocalPlayerTeam)
            {
                MoraleConstantsDef moraleDef = mech.Combat.Constants.GetActiveMoraleDef(mech.Combat);
                modifiers -= Math.Max(mech.Combat.LocalPlayerTeam.Morale - moraleDef.CanUseInspireLevel, 0);
            }

            if (modifiers < 0)
            {
                return(false);
            }

            var   rng = (new System.Random()).Next(100);
            float rollToBeat;

            if (!IsEarlyPanic)
            {
                rollToBeat = Math.Min(modifiers, Settings.MaxEjectChance);
            }
            else
            {
                rollToBeat = Math.Min(modifiers, Settings.MaxEjectChanceWhenEarly);
            }

            mech.Combat.MessageCenter.PublishMessage(!(rng < rollToBeat)
                ? new AddSequenceToStackMessage(new ShowActorInfoSequence(mech, $"Guts/Tactics Check Passed {Math.Floor(rollToBeat)}%", FloatieMessage.MessageNature.Buff, true))
                : new AddSequenceToStackMessage(new ShowActorInfoSequence(mech, $"Punchin' Out! {Math.Floor(rollToBeat)}%", FloatieMessage.MessageNature.Debuff, true)));

            return(rng < rollToBeat);
        }
コード例 #3
0
        public static bool Prefix(AttackDirector.AttackSequence __instance, ref WeaponHitInfo hitInfo, int groupIdx, int weaponIdx, Weapon weapon, ref float toHitChance, float prevDodgedDamage)
        {
            try
            {
                if (weapon.Type == WeaponType.SRM && weapon.AmmoCategoryValue.Name == "SRMStreak")
                {
                    Logger.Debug($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] ---");
                    Logger.Debug($"[AttackSequence_GetIndividualHits_PREFIX] ({weapon.parent.DisplayName}) PREPARE AttackSequence: {__instance.id}, WeaponGroup: {groupIdx}, Weapon: {weapon.Name}({weaponIdx})");
                    Logger.Info($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] ({weapon.Name}) toHitChance: {toHitChance}");

                    float hitChance = __instance.Director.Combat.ToHit.GetToHitChance(__instance.attacker, weapon, __instance.chosenTarget, __instance.attackPosition, __instance.chosenTarget.CurrentPosition, __instance.numTargets, __instance.meleeAttackType, __instance.isMoraleAttack);
                    Logger.Info($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.Name}) hitChance: {hitChance}");

                    float hitRoll = UnityEngine.Random.Range(0f, 1f);
                    Logger.Info($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.Name}) hitRoll: {hitRoll}");

                    bool streakWillHit = hitRoll <= hitChance;
                    Logger.Debug($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.Name}) streakWillHit: {streakWillHit}");


                    if (streakWillHit)
                    {
                        toHitChance = 1f;
                    }
                    else
                    {
                        toHitChance = 0f;
                    }
                    Logger.Info($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] ({weapon.Name}) toHitChance: {toHitChance}");


                    // Redirecting to hit determination method for clustered hits (LRMs only by default)
                    AttackSequenceGetClusteredHits.Invoke(__instance, new object[] { hitInfo, groupIdx, weaponIdx, weapon, toHitChance, prevDodgedDamage });
                    Logger.Info($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] ({weapon.Name}) Fetched clustered hits...");

                    if (streakWillHit)
                    {
                        // Make absolutely sure ALL missiles hits (Needed because of roll correction in AttackDirector.AttackSequence.GetClusteredHits() which sometimes returns "corrected" rolls of > 1f)
                        // With the added Patch to AttackDirector.AttackSequence.GetCorrectedRoll() this should NEVER be called
                        for (int i = 0; i < hitInfo.numberOfShots; i++)
                        {
                            // 0 = no hit, 65536 = secondary target hit
                            if (hitInfo.hitLocations[i] == 0 || hitInfo.hitLocations[i] == 65536)
                            {
                                Logger.Debug($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] WARNING: Missile[{i}] had a hit location of 0|65536 even though the Streak should hit. Recalculating!");
                                hitInfo.hitLocations[i] = __instance.chosenTarget.GetHitLocation(__instance.attacker, __instance.attackPosition, hitInfo.locationRolls[i], __instance.calledShotLocation, __instance.attacker.CalledShotBonusMultiplier);
                                hitInfo.hitPositions[i] = __instance.chosenTarget.GetImpactPosition(__instance.attacker, __instance.attackPosition, weapon, ref hitInfo.hitLocations[i], ref hitInfo.attackDirections[i], ref hitInfo.secondaryTargetIds[i], ref hitInfo.secondaryHitLocations[i]);
                            }
                        }
                    }
                    else
                    {
                        // Make absolutely sure NO (potential) missile would hit ANYTHING (They won't be fired anyway, but they NEED a valid hitInfo)
                        for (int i = 0; i < hitInfo.numberOfShots; i++)
                        {
                            if (hitInfo.hitLocations[i] != 0)
                            {
                                Logger.Debug($"[AttackDirector.AttackSequence_GetIndividualHits_PREFIX] WARNING: Missile[{i}] had a hit location != 0 even though the Streak should miss. Setting (imaginary) missVector manually!");
                                hitInfo.hitLocations[i] = 0;

                                Vector3 missVector = __instance.attackPosition + __instance.attacker.HighestLOSPosition;
                                Vector3 shotVector = __instance.chosenTarget.GameRep.GetMissPosition(missVector, weapon, __instance.Director.Combat.NetworkRandom);
                                Vector3 normalized = (shotVector - missVector).normalized;
                                shotVector = missVector + normalized * 500f;

                                hitInfo.hitPositions[i] = shotVector;
                            }
                        }
                    }
                    Utilities.LogHitLocations(hitInfo);

                    // SAFEGUARD: Clean hitInfo from potentially set secondary targets, there AREN'T secondary targets for Streaks
                    hitInfo.secondaryTargetIds = new string[hitInfo.numberOfShots];

                    // Hit locations are set, skipping original method...
                    return(false);
                }
                else
                {
                    return(true);
                }
            }
            catch (Exception e)
            {
                Logger.Error(e);
                return(true);
            }
        }
コード例 #4
0
        public static bool Prefix(AttackDirector.AttackSequence __instance, MessageCenterMessage message)
        {
            CustomAmmoCategoriesLog.Log.LogWrite("AttackDirector.AttackSequence.OnAttackSequenceResolveDamage");
            try
            {
                AttackSequenceResolveDamageMessage resolveDamageMessage = (AttackSequenceResolveDamageMessage)message;
                WeaponHitInfo hitInfo = resolveDamageMessage.hitInfo;
                if (hitInfo.attackSequenceId != __instance.id)
                {
                    return(true);
                }
                ;
                MessageCoordinator messageCoordinator = (MessageCoordinator)typeof(AttackDirector.AttackSequence).GetField("messageCoordinator", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
                if (!messageCoordinator.CanProcessMessage(resolveDamageMessage))
                {
                    messageCoordinator.StoreMessage((MessageCenterMessage)resolveDamageMessage);
                }
                else
                {
                    if (AttackDirector.AttackSequence.logger.IsLogEnabled)
                    {
                        AttackDirector.AttackSequence.logger.Log((object)string.Format("[OnAttackSequenceResolveDamage]  ID {0}, Group {1}, Weapon {2}, AttackerId [{3}], TargetId [{4}]", (object)__instance.id, (object)hitInfo.attackGroupIndex, (object)hitInfo.attackWeaponIndex, (object)hitInfo.attackerId, (object)hitInfo.targetId));
                    }
                    Weapon weapon = __instance.GetWeapon(resolveDamageMessage.hitInfo.attackGroupIndex, resolveDamageMessage.hitInfo.attackWeaponIndex);
                    if (__instance.meleeAttackType == MeleeAttackType.DFA)
                    {
                        float damageAmount = __instance.attacker.StatCollection.GetValue <float>("DFASelfDamage");
                        __instance.attacker.TakeWeaponDamage(resolveDamageMessage.hitInfo, 64, weapon, damageAmount, 0, DamageType.DFASelf);
                        __instance.attacker.TakeWeaponDamage(resolveDamageMessage.hitInfo, 128, weapon, damageAmount, 0, DamageType.DFASelf);
                        if (AttackDirector.damageLogger.IsLogEnabled)
                        {
                            AttackDirector.damageLogger.Log((object)string.Format("@@@@@@@@ {0} takes {1} damage to its legs from the DFA attack!", (object)__instance.attacker.DisplayName, (object)damageAmount));
                        }
                    }
                    __instance.target.ResolveWeaponDamage(resolveDamageMessage.hitInfo);
                    AbstractActor target      = __instance.target as AbstractActor;
                    int           attackIndex = -1;
                    int           num1        = 0;
                    int           num2        = 65536;
                    for (int index = 0; index < resolveDamageMessage.hitInfo.hitLocations.Length; ++index)
                    {
                        int hitLocation = resolveDamageMessage.hitInfo.hitLocations[index];
                        if (hitLocation != num1 && hitLocation != num2 && attackIndex == -1)
                        {
                            attackIndex = index;
                        }
                    }
                    if (attackIndex > -1)
                    {
                        //typeof(AttackDirector.AttackSequence).GetProperty("attackCompletelyMissed", BindingFlags.NonPublic).SetValue(__instance, (object)false);
                        PropertyInfo property = typeof(AttackDirector.AttackSequence).GetProperty("attackCompletelyMissed");
                        property.DeclaringType.GetProperty("attackCompletelyMissed");
                        property.GetSetMethod(true).Invoke(__instance, new object[1] {
                            (object)false
                        });
                        //__instance.attackCompletelyMissed = false;
                    }
                    if (attackIndex > -1 && !__instance.target.IsDead && target != null)
                    {
                        foreach (EffectData statusEffect in CustomAmmoCategories.getWeaponStatusEffects(weapon))
                        {
                            if (statusEffect.targetingData.effectTriggerType == EffectTriggerType.OnHit)
                            {
                                string effectID = string.Format("OnHitEffect_{0}_{1}", (object)__instance.attacker.GUID, (object)resolveDamageMessage.hitInfo.attackSequenceId);
                                if (statusEffect.Description == null || statusEffect.Description.Id == null || statusEffect.Description.Name == null)
                                {
                                    CustomAmmoCategoriesLog.Log.LogWrite($"WARNING: EffectID:{effectID} has broken effectDescId:{statusEffect?.Description.Id} effectDescName:{statusEffect?.Description.Name}! SKIPPING");
                                }
                                else
                                {
                                    CustomAmmoCategoriesLog.Log.LogWrite($"Applying effectID:{effectID} with effectDescId:{statusEffect?.Description.Id} effectDescName:{statusEffect?.Description.Name}");

                                    __instance.Director.Combat.EffectManager.CreateEffect(statusEffect, effectID, __instance.stackItemUID, (ICombatant)__instance.attacker, __instance.target, hitInfo, attackIndex, false);
                                    if (__instance.target != null)
                                    {
                                        __instance.Director.Combat.MessageCenter.PublishMessage((MessageCenterMessage) new FloatieMessage(__instance.target.GUID, __instance.target.GUID, statusEffect.Description.Name, FloatieMessage.MessageNature.Debuff));
                                    }
                                }
                            }
                        }
                        if (target != null)
                        {
                            List <EffectData> effectsForTriggerType = target.GetComponentStatusEffectsForTriggerType(EffectTriggerType.OnDamaged);
                            for (int index = 0; index < effectsForTriggerType.Count; ++index)
                            {
                                __instance.Director.Combat.EffectManager.CreateEffect(effectsForTriggerType[index], string.Format("OnDamagedEffect_{0}_{1}", (object)target.GUID, (object)resolveDamageMessage.hitInfo.attackSequenceId), __instance.stackItemUID, __instance.target, (ICombatant)__instance.attacker, hitInfo, attackIndex, false);
                            }
                        }
                    }
                    __instance.attacker.HandleDeath(__instance.attacker.GUID);
                    __instance.attacker.HandleDeath(__instance.attacker.GUID);
                    messageCoordinator.MessageComplete((MessageCenterMessage)resolveDamageMessage);
                }
            }
            catch (Exception e)
            {
                CustomAmmoCategoriesLog.Log.LogWrite("Exception " + e.ToString() + "\nFallback to default");
                return(true);
            }
            return(false);
        }
コード例 #5
0
        public static bool Prefix(AttackDirector.AttackSequence __instance, MessageCenterMessage message, List <List <Weapon> > ___sortedWeapons, ref int[][] ___numberOfShots, ref WeaponHitInfo?[][] ___weaponHitInfo)
        {
            try
            {
                AttackSequenceFireMessage attackSequenceFireMessage = (AttackSequenceFireMessage)message;
                if (attackSequenceFireMessage.sequenceId != __instance.id)
                {
                    return(false);
                }
                int    groupIdx  = attackSequenceFireMessage.groupIdx;
                int    weaponIdx = attackSequenceFireMessage.weaponIdx;
                Weapon weapon    = ___sortedWeapons[groupIdx][weaponIdx];

                Logger.Debug($"---");
                Logger.Debug($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.parent.DisplayName}) STARTED AttackSequence: {__instance.id}, WeaponGroup: {groupIdx}, Weapon: {weapon.Name}({weaponIdx})");

                //if(weapon.weaponDef.ComponentTags.Contains("component_type_srmstreak"))
                if (weapon.Type == WeaponType.SRM && weapon.AmmoCategoryValue.Name == "SRMStreak")
                {
                    WeaponHitInfo weaponHitInfo = ___weaponHitInfo[groupIdx][weaponIdx].Value;
                    bool          streakWillHit = weaponHitInfo.DidShotHitChosenTarget(0); // If first missile hits/misses, all will hit/miss
                    Logger.Info($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.Name}) streakWillHit: {streakWillHit}");

                    // Fire targeting laser
                    Vector3 floatieVector = new Vector3();
                    Utilities.CreateAndFireStreakTargetingLaser(__instance, weapon, out floatieVector, streakWillHit);

                    if (streakWillHit)
                    {
                        // Only floaties, everything else is prepared at this point

                        // Big Floatie
                        //__instance.Director.Combat.MessageCenter.PublishMessage(new FloatieMessage(__instance.chosenTarget.GUID, __instance.chosenTarget.GUID, "STREAK LOCKED-ON", FloatieMessage.MessageNature.CriticalHit));

                        // Small Floatie
                        FloatieMessage hitFloatie = new FloatieMessage(__instance.attacker.GUID, __instance.chosenTarget.GUID, "STREAK LOCKED-ON", __instance.Director.Combat.Constants.CombatUIConstants.floatieSizeMedium, FloatieMessage.MessageNature.Suppression, floatieVector.x, floatieVector.y, floatieVector.z);
                        __instance.Director.Combat.MessageCenter.PublishMessage(hitFloatie);

                        return(true);
                    }
                    else
                    {
                        // Cancel firing, see code of original method...

                        // Mark Streak SRMs as having fired nevertheless because a failed lock on should be handled like "fired"
                        new Traverse(weapon).Property("HasFired").SetValue(true);
                        weapon.CompleteFiring();
                        Logger.Info($"[AttackDirector.AttackSequence_OnAttackSequenceFire_PREFIX] ({weapon.Name}) HasFired: {weapon.HasFired}, RoundsSinceLastFire: {weapon.roundsSinceLastFire}");

                        // If weapon already prefired we would need to reincrement ammo (Note that Weapon.OffsetAmmo() is a custom extension method)
                        if (weapon.HasPreFired)
                        {
                            weapon.OffsetAmmo();
                        }

                        // Send out all necessary messages to keep the current AttackSequence in sync
                        AttackSequenceWeaponPreFireCompleteMessage weaponPreFireCompleteMessage = new AttackSequenceWeaponPreFireCompleteMessage(__instance.stackItemUID, __instance.id, groupIdx, weaponIdx);
                        __instance.Director.Combat.MessageCenter.PublishMessage(weaponPreFireCompleteMessage);

                        int numberOfShots = ___numberOfShots[groupIdx][weaponIdx];
                        for (int j = 0; j < numberOfShots; j++)
                        {
                            float hitDamage       = weapon.DamagePerShotAdjusted(weapon.parent.occupiedDesignMask);
                            float structureDamage = weapon.StructureDamagePerShotAdjusted(weapon.parent.occupiedDesignMask);
                            AttackSequenceImpactMessage impactMessage = new AttackSequenceImpactMessage(weaponHitInfo, j, hitDamage, structureDamage);
                            __instance.Director.Combat.MessageCenter.PublishMessage(impactMessage);
                        }

                        AttackSequenceResolveDamageMessage resolveDamageMessage = new AttackSequenceResolveDamageMessage(weaponHitInfo);
                        __instance.Director.Combat.MessageCenter.PublishMessage(resolveDamageMessage);

                        AttackSequenceWeaponCompleteMessage weaponCompleteMessage = new AttackSequenceWeaponCompleteMessage(__instance.stackItemUID, __instance.id, groupIdx, weaponIdx);
                        __instance.Director.Combat.MessageCenter.PublishMessage(weaponCompleteMessage);

                        // Big Floaties
                        //__instance.Director.Combat.MessageCenter.PublishMessage(new FloatieMessage(weapon.parent.GUID, weapon.parent.GUID, "STREAK LOCK-ON FAILED", FloatieMessage.MessageNature.Debuff));
                        //__instance.Director.Combat.MessageCenter.PublishMessage(new FloatieMessage(__instance.chosenTarget.GUID, __instance.chosenTarget.GUID, "STREAK LOCK-ON AVOIDED", FloatieMessage.MessageNature.Buff));

                        // Small Floatie
                        FloatieMessage missFloatie = new FloatieMessage(__instance.attacker.GUID, __instance.chosenTarget.GUID, "STREAK LOCK-ON FAILED", __instance.Director.Combat.Constants.CombatUIConstants.floatieSizeMedium, FloatieMessage.MessageNature.Dodge, floatieVector.x, floatieVector.y, floatieVector.z);
                        __instance.Director.Combat.MessageCenter.PublishMessage(missFloatie);



                        // Skip original method!
                        return(false);
                    }
                }

                return(true);
            }
            catch (Exception e)
            {
                Logger.Error(e);

                return(true);
            }
        }
コード例 #6
0
 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.Debuff));
             Fields.KeptPip = false;
         }
     }
     catch (Exception e)
     {
         Logger.Error(e);
     }
 }
コード例 #7
0
        private static void GetStreakHits(AttackDirector.AttackSequence instance, ref WeaponHitInfo hitInfo, int groupIdx, int weaponIdx, Weapon weapon, float toHitChance, float prevDodgedDamage)
        {
            CustomAmmoCategoriesLog.Log.LogWrite("GetStreakHits\n");
            if (hitInfo.numberOfShots == 0)
            {
                return;
            }
            ;
            if (AttackDirector.hitLogger.IsLogEnabled)
            {
                AttackDirector.hitLogger.Log((object)string.Format("???????? RANDOM HIT ROLLS (GetStreakHits): Weapon Group: {0} // Weapon: {1}", (object)groupIdx, (object)weaponIdx));
            }
            hitInfo.toHitRolls = instance.GetRandomNumbers(groupIdx, weaponIdx, hitInfo.numberOfShots);
            if (AttackDirector.hitLogger.IsLogEnabled)
            {
                AttackDirector.hitLogger.Log((object)string.Format("???????? RANDOM LOCATION ROLLS (GetStreakHits): Weapon Group: {0} // Weapon: {1}", (object)groupIdx, (object)weaponIdx));
            }
            hitInfo.locationRolls = instance.GetRandomNumbers(groupIdx, weaponIdx, hitInfo.numberOfShots);
            if (AttackDirector.hitLogger.IsLogEnabled)
            {
                AttackDirector.hitLogger.Log((object)string.Format("???????? DODGE ROLLS (GetStreakHits): Weapon Group: {0} // Weapon: {1}", (object)groupIdx, (object)weaponIdx));
            }
            hitInfo.dodgeRolls  = instance.GetRandomNumbers(groupIdx, weaponIdx, hitInfo.numberOfShots);
            hitInfo.hitVariance = instance.GetVarianceSums(groupIdx, weaponIdx, hitInfo.numberOfShots, weapon);
            int           previousHitLocation = 0;
            float         originalMultiplier  = 1f;
            float         adjacentMultiplier  = 1f;
            AbstractActor target         = instance.target as AbstractActor;
            Team          team           = weapon == null || weapon.parent == null || weapon.parent.team == null ? (Team)null : weapon.parent.team;
            bool          primeSucceeded = false;
            bool          primeFlag      = false;

            for (int hitIndex = 0; hitIndex < hitInfo.numberOfShots; ++hitIndex)
            {
                float corrRolls = (float)typeof(AttackDirector.AttackSequence).GetMethod("GetCorrectedRoll", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(instance, new object[2] {
                    (object)hitInfo.toHitRolls[hitIndex], (object)team
                });
                //bool succeeded = (double)instance.GetCorrectedRoll(hitInfo.toHitRolls[hitIndex], team) <= (double)toHitChance;
                bool succeeded = (double)corrRolls <= (double)toHitChance;
                if (team != null)
                {
                    team.ProcessRandomRoll(toHitChance, succeeded);
                }
                bool flag = false;
                if (target != null)
                {
                    flag = target.CheckDodge(instance.attacker, weapon, hitInfo, hitIndex, instance.IsBreachingShot);
                }
                if (hitIndex == 0)
                {
                    primeSucceeded = succeeded;
                    primeFlag      = flag;
                    CustomAmmoCategoriesLog.Log.LogWrite("  prime success:" + primeSucceeded + " dodge:" + primeFlag + "\n");
                }
                if (primeSucceeded && primeFlag)
                {
                    hitInfo.dodgeSuccesses[hitIndex] = true;
                    instance.FlagAttackContainsDodge();
                }
                else
                {
                    hitInfo.dodgeSuccesses[hitIndex] = false;
                }
                if (primeSucceeded && !primeFlag)
                {
                    if (previousHitLocation == 0)
                    {
                        previousHitLocation            = instance.target.GetHitLocation(instance.attacker, instance.attackPosition, hitInfo.locationRolls[hitIndex], instance.calledShotLocation, instance.attacker.CalledShotBonusMultiplier);
                        hitInfo.hitLocations[hitIndex] = previousHitLocation;
                        CustomAmmoCategoriesLog.Log.LogWrite("  hitLocation:" + hitInfo.hitLocations[hitIndex] + "\n");
                        if (AttackDirector.attackLogger.IsLogEnabled)
                        {
                            AttackDirector.attackLogger.Log((object)string.Format("SEQ:{0}: WEAP:{1} SHOT:{2} Initial streak hit! Location: {3}", (object)instance.id, (object)weaponIdx, (object)hitIndex, (object)hitInfo.hitLocations[hitIndex]));
                        }
                        if (AttackDirector.hitminLogger.IsLogEnabled)
                        {
                            AttackDirector.hitminLogger.Log((object)string.Format("WEAPON: {0} - SHOT: {1} Hits! ////// INITIAL HIT - HEX VAL {2}", (object)weapon.Name, (object)hitIndex, (object)hitInfo.hitLocations[hitIndex]));
                        }
                    }
                    else
                    {
                        hitInfo.hitLocations[hitIndex] = instance.target.GetAdjacentHitLocation(instance.attackPosition, hitInfo.locationRolls[hitIndex], previousHitLocation, originalMultiplier, adjacentMultiplier);
                        CustomAmmoCategoriesLog.Log.LogWrite("  hitLocation:" + hitInfo.hitLocations[hitIndex] + "\n");
                        if (AttackDirector.attackLogger.IsLogEnabled)
                        {
                            AttackDirector.attackLogger.Log((object)string.Format("SEQ:{0}: WEAP:{1} SHOT:{2} streak hit! Location: {3}", (object)instance.id, (object)weaponIdx, (object)hitIndex, (object)hitInfo.hitLocations[hitIndex]));
                        }
                        if (AttackDirector.hitminLogger.IsLogEnabled)
                        {
                            AttackDirector.hitminLogger.Log((object)string.Format("WEAPON: {0} - SHOT: {1} Hits! ////// STREAK HIT - HEX VAL {2}", (object)weapon.Name, (object)hitIndex, (object)hitInfo.hitLocations[hitIndex]));
                        }
                    }
                    hitInfo.hitQualities[hitIndex] = instance.Director.Combat.ToHit.GetBlowQuality(instance.attacker, instance.attackPosition, weapon, instance.target, instance.meleeAttackType, instance.IsBreachingShot);
                    instance.FlagShotHit();
                }
                else
                {
                    hitInfo.hitLocations[hitIndex] = 0;
                    CustomAmmoCategoriesLog.Log.LogWrite("  hitLocation:" + hitInfo.hitLocations[hitIndex] + "\n");
                    if (AttackDirector.attackLogger.IsLogEnabled)
                    {
                        AttackDirector.attackLogger.Log((object)string.Format("SEQ:{0}: WEAP:{1} SHOT:{2} Miss!", (object)instance.id, (object)weaponIdx, (object)hitIndex, (object)hitInfo.hitLocations[hitIndex]));
                    }
                    if (AttackDirector.hitminLogger.IsLogEnabled)
                    {
                        AttackDirector.hitminLogger.Log((object)string.Format("WEAPON: {0} - SHOT: {1} Misses!", (object)weapon.Name, (object)hitIndex));
                    }
                    instance.FlagShotMissed();
                }
                hitInfo.hitPositions[hitIndex] = instance.target.GetImpactPosition(instance.attacker, instance.attackPosition, weapon, ref hitInfo.hitLocations[hitIndex]);
            }
        }
コード例 #8
0
        public static bool Prefix(AttackDirector.AttackSequence __instance, Weapon weapon, int groupIdx, int weaponIdx, int numberOfShots, bool indirectFire, float dodgedDamage, ref WeaponHitInfo __result)
        {
            WeaponHitInfo hitInfo = new WeaponHitInfo();

            hitInfo.attackerId        = __instance.attacker.GUID;
            hitInfo.targetId          = __instance.target.GUID;
            hitInfo.numberOfShots     = numberOfShots;
            hitInfo.stackItemUID      = __instance.stackItemUID;
            hitInfo.attackSequenceId  = __instance.id;
            hitInfo.attackGroupIndex  = groupIdx;
            hitInfo.attackWeaponIndex = weaponIdx;
            hitInfo.toHitRolls        = new float[numberOfShots];
            hitInfo.locationRolls     = new float[numberOfShots];
            hitInfo.dodgeRolls        = new float[numberOfShots];
            hitInfo.dodgeSuccesses    = new bool[numberOfShots];
            hitInfo.hitLocations      = new int[numberOfShots];
            hitInfo.hitPositions      = new Vector3[numberOfShots];
            hitInfo.hitVariance       = new int[numberOfShots];
            hitInfo.hitQualities      = new AttackImpactQuality[numberOfShots];
            if (AttackDirector.hitLogger.IsLogEnabled)
            {
                Vector3         collisionWorldPos;
                LineOfFireLevel lineOfFire           = __instance.Director.Combat.LOS.GetLineOfFire(__instance.attacker, __instance.attackPosition, __instance.target, __instance.target.CurrentPosition, __instance.target.CurrentRotation, out collisionWorldPos);
                float           allModifiers         = __instance.Director.Combat.ToHit.GetAllModifiers(__instance.attacker, weapon, __instance.target, __instance.attackPosition + __instance.attacker.HighestLOSPosition, __instance.target.TargetPosition, lineOfFire, __instance.isMoraleAttack);
                string          modifiersDescription = __instance.Director.Combat.ToHit.GetAllModifiersDescription(__instance.attacker, weapon, __instance.target, __instance.attackPosition + __instance.attacker.HighestLOSPosition, __instance.target.TargetPosition, lineOfFire, __instance.isMoraleAttack);
                Pilot           pilot = __instance.attacker.GetPilot();
                AttackDirector.hitLogger.Log((object)string.Format("======================================== Unit Firing: {0} | Weapon: {1} | Shots: {2}", (object)__instance.attacker.DisplayName, (object)weapon.Name, (object)numberOfShots));
                AttackDirector.hitLogger.Log((object)string.Format("======================================== Hit Info: GROUP {0} | ID {1}", (object)groupIdx, (object)weaponIdx));
                AttackDirector.hitLogger.Log((object)string.Format("======================================== MODIFIERS: {0}... FINAL: [[ {1} ]] ", (object)modifiersDescription, (object)allModifiers));
                if (pilot != null)
                {
                    AttackDirector.hitLogger.Log((object)__instance.Director.Combat.ToHit.GetBaseToHitChanceDesc(__instance.attacker));
                }
                else
                {
                    AttackDirector.hitLogger.Log((object)string.Format("======================================== Gunnery Check: NO PILOT"));
                }
            }
            float toHitChance = __instance.Director.Combat.ToHit.GetToHitChance(__instance.attacker, weapon, __instance.target, __instance.attackPosition, __instance.target.CurrentPosition, __instance.numTargets, __instance.meleeAttackType, __instance.isMoraleAttack);

            if (Mech.TEST_KNOCKDOWN)
            {
                toHitChance = 1f;
            }
            if (AttackDirector.hitLogger.IsLogEnabled)
            {
                AttackDirector.hitLogger.Log((object)string.Format("======================================== HIT CHANCE: [[ {0:P2} ]]", (object)toHitChance));
            }
            hitInfo.attackDirection       = __instance.Director.Combat.HitLocation.GetAttackDirection(__instance.attackPosition, __instance.target);
            hitInfo.attackDirectionVector = __instance.Director.Combat.HitLocation.GetAttackDirectionVector(__instance.attackPosition, __instance.target);
            object[]         args       = new object[6];
            HitGeneratorType hitGenType = CustomAmmoCategories.getHitGenerator(weapon);

            switch (hitGenType)
            {
            case HitGeneratorType.Individual:
                args[0] = hitInfo; args[1] = groupIdx; args[2] = weaponIdx; args[3] = weapon; args[4] = toHitChance; args[5] = dodgedDamage;
                typeof(AttackDirector.AttackSequence).GetMethod("GetIndividualHits", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(__instance, args);
                hitInfo = (WeaponHitInfo)args[0];
                break;

            case HitGeneratorType.Cluster:
                args[0] = hitInfo; args[1] = groupIdx; args[2] = weaponIdx; args[3] = weapon; args[4] = toHitChance; args[5] = dodgedDamage;
                typeof(AttackDirector.AttackSequence).GetMethod("GetClusteredHits", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(__instance, args);
                hitInfo = (WeaponHitInfo)args[0];
                break;

            case HitGeneratorType.Streak:
                AttackSequence_GenerateHitInfo.GetStreakHits(__instance, ref hitInfo, groupIdx, weaponIdx, weapon, toHitChance, dodgedDamage);
                break;

            default:
                AttackDirector.attackLogger.LogError((object)string.Format("GenerateHitInfo found invalid weapon type: {0}, using basic hit info", (object)hitGenType));
                args[0] = hitInfo; args[1] = groupIdx; args[2] = weaponIdx; args[3] = weapon; args[4] = toHitChance; args[5] = dodgedDamage;
                typeof(AttackDirector.AttackSequence).GetMethod("GetIndividualHits", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(__instance, args);
                hitInfo = (WeaponHitInfo)args[0];
                break;
            }
            __result = hitInfo;
            return(false);
            //return hitInfo;
        }
コード例 #9
0
        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.");
        }