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()}"); }
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; } }
public static string CacheKey(AbstractActor attacker, ICombatant target) { return($"{attacker.DistinctId()}-{target.DistinctId()}"); }
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); }
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); }
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!"); } }