// This patch needs to run to correctly fix the melee attack to the target selected by CleverGirl/AI.
 //   During AI eval multiple targets are evaluated, and this ensures we use the attack for the one that was picked.
 static void Postfix(AbstractActor unit, ref BehaviorTreeResults __result)
 {
     if (__result != null && __result.nodeState == BehaviorNodeState.Success && __result.orderInfo is AttackOrderInfo attackOrderInfo)
     {
         if (attackOrderInfo.IsMelee)
         {
             Mod.Log.Debug?.Write($"Setting melee weapon for attack from attacker: {unit?.DistinctId()} versus target: {attackOrderInfo.TargetUnit?.DistinctId()}");
             // Create melee options
             MeleeState meleeState = ModState.AddorUpdateMeleeState(unit, attackOrderInfo.AttackFromLocation, attackOrderInfo.TargetUnit);
             if (meleeState != null)
             {
                 MeleeAttack meleeAttack = meleeState.GetHighestDamageAttackForUI();
                 ModState.AddOrUpdateSelectedAttack(unit, meleeAttack);
             }
         }
         else if (attackOrderInfo.IsDeathFromAbove)
         {
             // Create melee options
             MeleeState meleeState = ModState.AddorUpdateMeleeState(unit, attackOrderInfo.AttackFromLocation, attackOrderInfo.TargetUnit);
             if (meleeState != null)
             {
                 MeleeAttack meleeAttack = meleeState.DFA;
                 ModState.AddOrUpdateSelectedAttack(unit, meleeAttack);
             }
         }
     }
     else
     {
         Mod.Log.Trace?.Write($"BehaviorTree result is not failed: {__result?.nodeState} or is not an attackOrderInfo, skipping.");
     }
 }
        static void Prefix(CombatHUDAttackModeSelector __instance, CombatHUDFireButton.FireMode mode,
                           ref string additionalDetails, bool showHeatWarnings)
        {
            Mod.UILog.Trace?.Write($"ShowFireButton called with mode: {mode}");

            // Intentionally regen the meleeStates everytime the button changes, to make sure different positions calculate properly
            if (mode == CombatHUDFireButton.FireMode.Engage || mode == CombatHUDFireButton.FireMode.DFA)
            {
                if (SharedState.CombatHUD?.SelectionHandler?.ActiveState?.PreviewPos != ModState.MeleePreviewPos)
                {
                    ModState.MeleePreviewPos = SharedState.CombatHUD.SelectionHandler.ActiveState.PreviewPos;

                    // Update melee states
                    ModState.AddorUpdateMeleeState(
                        SharedState.CombatHUD.SelectionHandler.ActiveState.SelectedActor,
                        SharedState.CombatHUD.SelectionHandler.ActiveState.PreviewPos,
                        SharedState.CombatHUD.SelectionHandler.ActiveState.TargetedCombatant);
                    Mod.UILog.Debug?.Write($"Updated melee state for position: {ModState.MeleePreviewPos}");

                    // Re-enable any buttons if they were disabled.
                    __instance.FireButton.SetState(ButtonState.Enabled);
                    __instance.DescriptionContainer.SetActive(true);
                }
            }
            else
            {
                ModState.InvalidateState(SharedState.CombatHUD?.SelectionHandler?.ActiveState?.SelectedActor);
            }
        }
Exemplo n.º 3
0
        static void Postfix(MechMeleeSequence __instance, Mech mech, ICombatant meleeTarget,
                            List <Weapon> requestedWeapons, Vector3 desiredMeleePosition)
        {
            try
            {
                // Find the selectedAttack we should use for this sequence
                MeleeAttack selectedAttack = ModState.GetSelectedAttack(mech);
                if (selectedAttack == null)
                {
                    Mod.MeleeLog.Warn?.Write($"Melee sequence {__instance.SequenceGUID} has no pre-selected attack state, will have to autoselected. Let Frost know as this should not happen!");
                    MeleeState meleeState = ModState.AddorUpdateMeleeState(mech, desiredMeleePosition, meleeTarget as AbstractActor);
                    if (meleeState == null)
                    {
                        Mod.Log.Error?.Write($"Could not build melee state for selected melee attack - this should NEVER happen!");
                        return;
                    }
                    selectedAttack = meleeState.GetHighestDamageAttackForUI();
                }

                if (selectedAttack == null || !selectedAttack.IsValid)
                {
                    Mod.Log.Error?.Write($"Could not select a valid attack for the selected sequence - this should NEVER happen!");
                    return;
                }

                // Check to see if we have an imaginary weapon to use; if not create it
                (Weapon meleeWeapon, Weapon dfaWeapon)weapons = ModState.GetFakedWeapons(mech);

                // Create the weapon + representation
                ModState.AddOrUpdateMeleeSequenceState(__instance.SequenceGUID, selectedAttack, weapons.meleeWeapon);

                StringBuilder sb = new StringBuilder();
                foreach (Weapon weapon in requestedWeapons)
                {
                    sb.Append(weapon.UIName);
                    sb.Append(",");
                }
                Mod.MeleeLog.Info?.Write($"  -- Initial requested weapons: {sb}");

                isValid = true;
            }
            catch (Exception e)
            {
                Mod.Log.Error?.Write(e, $"Failed to initialize Melee sequence {__instance.SequenceGUID}!");
            }
        }
        static void Prefix(AIUtil __instance, AbstractActor unit, AttackType attackType, List <Weapon> weaponList,
                           ICombatant target, Vector3 attackPosition, Vector3 targetPosition, bool useRevengeBonus,
                           AbstractActor unitForBVContext)
        {
            Mech          attackingMech = unit as Mech;
            AbstractActor targetActor   = target as AbstractActor;
            Mech          targetMech    = target as Mech;

            Mod.AILog.Info?.Write("AITUIL_EDFA - entered.");

            if (attackingMech == null || targetActor == null)
            {
                return;                                               // Nothing to do
            }
            if (attackType == AttackType.Shooting || attackType == AttackType.None || attackType == AttackType.Count)
            {
                return;                                                                                                       // nothing to do
            }
            try
            {
                Mod.AILog.Info?.Write($"=== Calculating expectedDamage for {attackingMech.DistinctId()} melee attack " +
                                      $"from position: {attackPosition} against target: {target.DistinctId()} at position: {targetPosition}");
                Mod.AILog.Info?.Write($"  useRevengeBonus: {useRevengeBonus}");
                Mod.AILog.Info?.Write($"  --- weaponList:");
                if (weaponList != null)
                {
                    foreach (Weapon weapon in weaponList)
                    {
                        Mod.AILog.Info?.Write($"      {weapon?.UIName}");
                    }
                }

                bool        modifyAttack = false;
                MeleeAttack meleeAttack  = null;
                Weapon      meleeWeapon  = null;

                bool isCharge = false;
                bool isMelee  = false;
                if (attackType == AttackType.Melee && attackingMech?.Pathing?.GetMeleeDestsForTarget(targetActor)?.Count > 0)
                {
                    Mod.AILog.Info?.Write($"Modifying {attackingMech.DistinctId()}'s melee attack damage for utility");

                    // Create melee options
                    MeleeState meleeState = ModState.AddorUpdateMeleeState(attackingMech, attackPosition, targetActor);
                    if (meleeState != null)
                    {
                        meleeAttack = meleeState.GetHighestDamageAttackForUI();
                        ModState.AddOrUpdateSelectedAttack(attackingMech, meleeAttack);
                        if (meleeAttack is ChargeAttack)
                        {
                            isCharge = true;
                        }

                        meleeWeapon  = attackingMech.MeleeWeapon;
                        modifyAttack = true;
                        isMelee      = true;
                    }
                }

                bool isDFA = false;
                if (attackType == AttackType.DeathFromAbove && attackingMech?.JumpPathing?.GetDFADestsForTarget(targetActor)?.Count > 0)
                {
                    Mod.AILog.Info?.Write($"Modifying {attackingMech.DistinctId()}'s DFA attack damage for utility");

                    // Create melee options
                    MeleeState meleeState = ModState.AddorUpdateMeleeState(attackingMech, attackPosition, targetActor);
                    if (meleeState != null)
                    {
                        meleeAttack = meleeState.DFA;
                        ModState.AddOrUpdateSelectedAttack(attackingMech, meleeAttack);

                        meleeWeapon  = attackingMech.DFAWeapon;
                        modifyAttack = true;
                        isDFA        = true;
                    }
                }

                // No pathing dests for melee or DFA - skip
                if (!isMelee && !isDFA)
                {
                    return;
                }

                if (modifyAttack && meleeAttack == null || !meleeAttack.IsValid)
                {
                    Mod.AILog.Info?.Write($"Failed to find a valid melee state, marking melee weapons as 1 damage.");
                    meleeWeapon.StatCollection.Set <float>(ModStats.HBS_Weapon_DamagePerShot, 0);
                    meleeWeapon.StatCollection.Set <float>(ModStats.HBS_Weapon_Instability, 0);
                    return;
                }

                if (modifyAttack && meleeAttack != null && meleeAttack.IsValid)
                {
                    Mod.AILog.Info?.Write($"Evaluating utility against state: {meleeAttack.Label}");
                    // Set the DFA weapon's damage to our expected damage
                    float totalDamage = meleeAttack.TargetDamageClusters.Sum();
                    Mod.AILog.Info?.Write($" - totalDamage: {totalDamage}");

                    // Check to see if the attack will unsteady a target
                    float evasionBreakUtility = 0f;
                    if (targetMech != null && targetMech.EvasivePipsCurrent > 0 &&
                        (meleeAttack.OnTargetMechHitForceUnsteady || AttackHelper.WillUnsteadyTarget(meleeAttack.TargetInstability, targetMech))
                        )
                    {
                        // Target will lose their evasion pips
                        evasionBreakUtility = targetMech.EvasivePipsCurrent * Mod.Config.Melee.AI.EvasionPipRemovedUtility;
                        Mod.AILog.Info?.Write($"  Adding {evasionBreakUtility} virtual damage to EV from " +
                                              $"evasivePips: {targetMech.EvasivePipsCurrent} x bonusDamagePerPip: {Mod.Config.Melee.AI.EvasionPipRemovedUtility}");
                    }

                    float knockdownUtility = 0f;
                    if (targetMech != null && targetMech.pilot != null &&
                        AttackHelper.WillKnockdownTarget(meleeAttack.TargetInstability, targetMech, meleeAttack.OnTargetMechHitForceUnsteady))
                    {
                        float centerTorsoArmorAndStructure = targetMech.GetMaxArmor(ArmorLocation.CenterTorso) + targetMech.GetMaxStructure(ChassisLocations.CenterTorso);
                        if (AttackHelper.WillInjuriesKillTarget(targetMech, 1))
                        {
                            knockdownUtility = centerTorsoArmorAndStructure * Mod.Config.Melee.AI.PilotInjuryMultiUtility;
                            Mod.AILog.Info?.Write($"  Adding {knockdownUtility} virtual damage to EV from " +
                                                  $"centerTorsoArmorAndStructure: {centerTorsoArmorAndStructure} x injuryMultiUtility: {Mod.Config.Melee.AI.PilotInjuryMultiUtility}");
                        }
                        else
                        {
                            // Attack won't kill, so only apply a fraction equal to the totalHeath
                            float injuryFraction = (targetMech.pilot.TotalHealth - 1) - (targetMech.pilot.Injuries + 1);
                            knockdownUtility = (centerTorsoArmorAndStructure * Mod.Config.Melee.AI.PilotInjuryMultiUtility) / injuryFraction;
                            Mod.AILog.Info?.Write($"  Adding {knockdownUtility} virtual damage to EV from " +
                                                  $"(centerTorsoArmorAndStructure: {centerTorsoArmorAndStructure} x injuryMultiUtility: {Mod.Config.Melee.AI.PilotInjuryMultiUtility}) " +
                                                  $"/ injuryFraction: {injuryFraction}");
                        }
                    }

                    // Check to see how much evasion loss the attacker will have
                    //  use current pips + any pips gained from movement (charge)
                    float distance      = (attackPosition + unit.CurrentPosition).magnitude;
                    int   newPips       = unit.GetEvasivePipsResult(distance, isDFA, isCharge, true);
                    int   normedNewPips = (unit.EvasivePipsCurrent + newPips) > unit.StatCollection.GetValue <int>("MaxEvasivePips") ?
                                          unit.StatCollection.GetValue <int>("MaxEvasivePips") : (unit.EvasivePipsCurrent + newPips);
                    float selfEvasionDamage = 0f;
                    if (meleeAttack.UnsteadyAttackerOnHit || meleeAttack.UnsteadyAttackerOnMiss)
                    {
                        // TODO: Should evaluate chance to hit, and apply these partial damage based upon success chances
                        selfEvasionDamage = normedNewPips * Mod.Config.Melee.AI.EvasionPipLostUtility;
                        Mod.AILog.Info?.Write($"  Reducing virtual damage by {selfEvasionDamage} due to potential loss of {normedNewPips} pips.");
                    }

                    // Check to see how much damage the attacker will take
                    float selfDamage = 0f;
                    if (meleeAttack.AttackerDamageClusters.Length > 0)
                    {
                        selfDamage = meleeAttack.AttackerDamageClusters.Sum();
                        Mod.AILog.Info?.Write($"  Reducing virtual damage by {selfDamage} due to attacker damage on attack.");
                    }

                    float virtualDamage = totalDamage + evasionBreakUtility + knockdownUtility - selfEvasionDamage - selfDamage;
                    Mod.AILog.Info?.Write($"  Virtual damage calculated as {virtualDamage} = " +
                                          $"totalDamage: {totalDamage} + evasionBreakUtility: {evasionBreakUtility} + knockdownUtility: {knockdownUtility}" +
                                          $" - selfDamage: {selfDamage} - selfEvasionDamage: {selfEvasionDamage}");

                    Mod.AILog.Info?.Write($"Setting weapon: {meleeWeapon.UIName} to virtual damage: {virtualDamage} for EV calculation");
                    meleeWeapon.StatCollection.Set <float>(ModStats.HBS_Weapon_DamagePerShot, virtualDamage);
                    meleeWeapon.StatCollection.Set <float>(ModStats.HBS_Weapon_Instability, 0);

                    Mod.AILog.Info?.Write($"=== Done modifying attack!");
                }
                else
                {
                    Mod.AILog.Debug?.Write($"Attack is not melee {modifyAttack}, or melee state is invalid or null. I assume the normal AI will prevent action.");
                }
            }
            catch (Exception e)
            {
                Mod.AILog.Error?.Write(e, $"Failed to calculate melee damage for {unit.DistinctId()} using attackType {attackType} due to error!");
            }
        }