static void Postfix(AIUtil __instance, AbstractActor unit, AttackType attackType, List <Weapon> weaponList,
                     ICombatant target, Vector3 attackPosition, Vector3 targetPosition, bool useRevengeBonus,
                     AbstractActor unitForBVContext, float __result)
 {
     Mod.AILog.Info?.Write($"=== Expected damage of: {__result} for attacker {unit.DistinctId()} using attackType: " +
                           $"{attackType} versus target: {target.DistinctId()}");
 }
Example #2
0
        static void Postfix(Mech attacker, ICombatant target, ref MeleeAttackType __result)
        {
            MeleeAttack selectedAttack = ModState.GetSelectedAttack(attacker);

            if (selectedAttack != null)
            {
                Mod.Log.Info?.Write($"Forcing melee animation to {selectedAttack.AttackAnimation} for " +
                                    $"attacker: {attacker.DistinctId()} vs. target: {target.DistinctId()}");
                __result = selectedAttack.AttackAnimation;
            }
        }
        private void ResolveNextAttack()
        {
            this.timeSinceLastAttack += Time.deltaTime;
            if (this.timeSinceLastAttack > this.timeBetweenAttacks)
            {
                if (this.AttackingActors.Count > 0)
                {
                    AbstractActor actor = this.AttackingActors[0];
                    this.AttackingActors.RemoveAt(0);
                    Mod.Log.Info?.Write($"Ambush attack from actor: {actor.DistinctId()}");

                    // Find the closest target
                    ICombatant closestTarget = AllTargets[0];
                    foreach (ICombatant target in AllTargets)
                    {
                        if ((target.CurrentPosition - actor.CurrentPosition).magnitude <
                            (closestTarget.CurrentPosition - actor.CurrentPosition).magnitude)
                        {
                            closestTarget = target;
                        }
                    }

                    float currentRange = (closestTarget.CurrentPosition - actor.CurrentPosition).magnitude;
                    Mod.Log.Info?.Write($" -- ambush attack targets closest actor: {closestTarget.DistinctId()} at distance: {currentRange}");

                    List <Weapon> selectedWeapons = new List <Weapon>();
                    foreach (Weapon weapon in actor.Weapons)
                    {
                        if (weapon.CanFire && weapon.MinRange < currentRange)
                        {
                            selectedWeapons.Add(weapon);
                            Mod.Log.Info?.Write($" -- ambush weapon: {weapon.UIName}");
                        }
                    }

                    AttackStackSequence attackSequence = new AttackStackSequence(actor, closestTarget, actor.CurrentPosition, actor.CurrentRotation,
                                                                                 selectedWeapons);
                    ModState.Combat.MessageCenter.PublishMessage(new AddSequenceToStackMessage(attackSequence));
                }
                this.timeSinceLastAttack = 0f;
            }
        }
Example #4
0
 public static string CacheKey(AbstractActor attacker, ICombatant target)
 {
     return($"{attacker.DistinctId()}-{target.DistinctId()}");
 }
Example #5
0
        public static float GetTonnage(ICombatant combatant)
        {
            float tonnage = 0f;

            if (combatant == null)
            {
                Mod.Log.Debug?.Write($"Combatant is null, using tonnage of 0!");
            }
            else if (combatant is BattleTech.Building)
            {
                Mod.Log.Debug?.Write($"Using virtual tonnage: {Mod.Config.VirtualTonnage.Building} for building: {combatant.DistinctId()}");
                return(Mod.Config.VirtualTonnage.Building);
            }
            else if (combatant is Turret turret)
            {
                TagSet actorTags = turret.GetTags();
                if (actorTags != null && actorTags.Contains("unit_light"))
                {
                    tonnage = Mod.Config.VirtualTonnage.LightTurret;
                }
                else if (actorTags != null && actorTags.Contains("unit_medium"))
                {
                    tonnage = Mod.Config.VirtualTonnage.MediumTurret;
                }
                else if (actorTags != null && actorTags.Contains("unit_heavy"))
                {
                    tonnage = Mod.Config.VirtualTonnage.HeavyTurret;
                }
                else
                {
                    tonnage = Mod.Config.VirtualTonnage.DefaultTurret;
                }
                Mod.Log.Debug?.Write($"Using virtual tonnage: {tonnage} for turret: {turret.DistinctId()}");
                return(tonnage > Mod.Config.TonnageCap ? Mod.Config.TonnageCap : tonnage);
            }
            else if (combatant is Mech mech)
            {
                Mod.Log.Debug?.Write($"Using tonnage: {mech.tonnage} for mech: {mech.DistinctId()}");
                return(mech.tonnage > Mod.Config.TonnageCap ? Mod.Config.TonnageCap : mech.tonnage);
            }
            else if (combatant is Vehicle vehicle)
            {
                Mod.Log.Debug?.Write($"Using tonnage: {vehicle.tonnage} for vehicle: {vehicle.DistinctId()}");
                return(vehicle.tonnage > Mod.Config.TonnageCap ? Mod.Config.TonnageCap : vehicle.tonnage);
            }

            return(tonnage > Mod.Config.TonnageCap ? Mod.Config.TonnageCap : tonnage);
        }
Example #6
0
        public static void CreateImaginaryAttack(Mech attacker, Weapon attackWeapon, ICombatant target, int weaponHitInfoStackItemUID, float[] damageClusters,
                                                 DamageType damageType, MeleeAttackType attackType)
        {
            Mod.Log.Info?.Write($"  Creating imaginary attack for attacker: {attacker.DistinctId()} at position: {attacker?.CurrentPosition} and rot: {attacker?.CurrentRotation}  " +
                                $"vs. target: {target.DistinctId()} at position: {target?.CurrentPosition} and rot: {target?.CurrentRotation}  " +
                                $"using weapon =>  isNull: {attackWeapon == null}  name: {attackWeapon?.Name}  damageType: {damageType}  attackType: {attackType}");

            if (attackWeapon.ammo() == null)
            {
                Mod.Log.Error?.Write($"AMMO is null!");
            }
            if (attackWeapon.mode() == null)
            {
                Mod.Log.Error?.Write($"Mode is null!");
            }
            if (attackWeapon.exDef() == null)
            {
                Mod.Log.Error?.Write($"exDef is null!");
            }

            AttackDirector.AttackSequence attackSequence = target.Combat.AttackDirector.CreateAttackSequence(0, attacker, target,
                                                                                                             attacker.CurrentPosition, attacker.CurrentRotation, 0, new List <Weapon>()
            {
                attackWeapon
            },
                                                                                                             attackType, 0, false
                                                                                                             );

            AttackDirection[] attackDirections = new AttackDirection[damageClusters.Length];
            WeaponHitInfo     hitInfo          = new WeaponHitInfo(0, attackSequence.id, 0, 0, attacker.GUID, target.GUID, 1,
                                                                   null, null, null, null, null, null, null, attackDirections, null, null, null)
            {
                attackerId    = attacker.GUID,
                targetId      = target.GUID,
                numberOfShots = damageClusters.Length,
                stackItemUID  = weaponHitInfoStackItemUID,
                locationRolls = new float[damageClusters.Length],
                hitLocations  = new int[damageClusters.Length],
                hitPositions  = new Vector3[damageClusters.Length],
                hitQualities  = new AttackImpactQuality[damageClusters.Length]
            };

            AttackDirection attackDirection = attacker.Combat.HitLocation.GetAttackDirection(attacker, target);

            Mod.Log.Info?.Write($"  Attack direction is: {attackDirection}");

            int i = 0;

            foreach (int damage in damageClusters)
            {
                // Set hit qualities
                hitInfo.attackDirections[i] = attackDirection;
                hitInfo.hitQualities[i]     = AttackImpactQuality.Solid;
                hitInfo.hitPositions[i]     = attacker.CurrentPosition;

                float adjustedDamage = damage;
                float randomRoll     = (float)Mod.Random.NextDouble();
                if (target is Mech mech)
                {
                    ArmorLocation location =
                        SharedState.Combat.HitLocation.GetHitLocation(attacker.CurrentPosition, mech, randomRoll, ArmorLocation.None, 0f);
                    hitInfo.hitLocations[i] = (int)location;

                    adjustedDamage = mech.GetAdjustedDamageForMelee(damage, attackWeapon.WeaponCategoryValue);
                    Mod.Log.Info?.Write($"  {adjustedDamage} damage to location: {location}");
                    ShowDamageFloatie(mech, location, adjustedDamage, hitInfo.attackerId);
                }
                else if (target is Vehicle vehicle)
                {
                    VehicleChassisLocations location =
                        SharedState.Combat.HitLocation.GetHitLocation(attacker.CurrentPosition, vehicle, randomRoll, VehicleChassisLocations.None, 0f);
                    hitInfo.hitLocations[i] = (int)location;

                    adjustedDamage = vehicle.GetAdjustedDamageForMelee(damage, attackWeapon.WeaponCategoryValue);
                    Mod.Log.Info?.Write($"  {adjustedDamage} damage to location: {location}");
                    ShowDamageFloatie(vehicle, location, adjustedDamage, hitInfo.attackerId);
                }
                else if (target is Turret turret)
                {
                    BuildingLocation location = BuildingLocation.Structure;
                    hitInfo.hitLocations[i] = (int)BuildingLocation.Structure;

                    adjustedDamage = turret.GetAdjustedDamageForMelee(damage, attackWeapon.WeaponCategoryValue);
                    Mod.Log.Info?.Write($"  {adjustedDamage} damage to location: {location}");
                    ShowDamageFloatie(turret, adjustedDamage, hitInfo.attackerId);
                }

                // Make the target take weapon damage
                target.TakeWeaponDamage(hitInfo, hitInfo.hitLocations[i], attackWeapon, adjustedDamage, 0, 0, damageType);

                i++;
            }

            // Cleanup after myself
            target.Combat.AttackDirector.RemoveAttackSequence(attackSequence);
        }
Example #7
0
        static bool Prefix(ref BehaviorTreeResults __result,
                           string ___name, BehaviorTree ___tree, AbstractActor ___unit)
        {
            Mod.AILog.Info?.Write("CanMeleeHostileTargetsNode:Tick() invoked.");

            if (!(___unit is Mech))
            {
                // Not a mech, so don't allow them to melee
                __result = new BehaviorTreeResults(BehaviorNodeState.Failure);
                return(false);
            }

            //bool flag = false;
            //for (int i = 0; i < unit.Weapons.Count; i++)
            //{
            //	if (unit.Weapons[i].CanFire)
            //	{
            //		flag = true;
            //		break;
            //	}
            //}

            Mod.AILog.Info?.Write($"AI attacker: {___unit.DistinctId()} has {___unit.BehaviorTree.enemyUnits.Count} enemy units.");
            for (int j = 0; j < ___unit.BehaviorTree.enemyUnits.Count; j++)
            {
                ICombatant targetCombatant = ___unit.BehaviorTree.enemyUnits[j];

                // Skip the retaliation check; melee w/ kick is always viable, as is charge
                //Mech targetMech = targetCombatant as Mech;
                //if (targetMech != null)
                //{
                //	float num = AIUtil.ExpectedDamageForMeleeAttackUsingUnitsBVs(targetMech, unit, targetMech.CurrentPosition, mech.CurrentPosition, useRevengeBonus: false, unit);
                //	float num2 = AIUtil.ExpectedDamageForMeleeAttackUsingUnitsBVs(mech, targetMech, mech.CurrentPosition, targetMech.CurrentPosition, useRevengeBonus: false, unit);
                //	if (num2 <= 0f)
                //	{
                //		continue;
                //	}
                //	float num3 = num / num2;
                //	if (flag2 && num3 > unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_MeleeDamageRatioCap).FloatVal)
                //	{
                //		continue;
                //	}
                //}

                if (___unit.CanEngageTarget(targetCombatant))
                {
                    Mod.AILog.Info?.Write($"AI attacker: {___unit.DistinctId()} can engage target: {targetCombatant.DistinctId()}, returning true nodeState.");
                    __result = new BehaviorTreeResults(BehaviorNodeState.Success);
                    return(false);
                }
                else
                {
                    Mod.AILog.Info?.Write($"AI attacker: {___unit.DistinctId()} can NOT engage target: {targetCombatant.DistinctId()}");
                }
            }

            Mod.AILog.Info?.Write($"AI source: {___unit.DistinctId()} could find no targets, skipping.");
            // Fall through - couldn't find a single enemy unit we could attack, so skip
            __result = new BehaviorTreeResults(BehaviorNodeState.Failure);
            return(false);
        }
        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!");
            }
        }