static bool ValidateMultiAttackOrder(MultiTargetAttackOrderInfo order, AbstractActor unit) { AIUtil.LogAI("Multiattack validation", unit); for (int subAttackIndex = 0; subAttackIndex < order.SubTargetOrders.Count; ++subAttackIndex) { AttackOrderInfo subOrder = order.SubTargetOrders[subAttackIndex]; AIUtil.LogAI(string.Format("SubAttack #{0}: target {1} {2}", subAttackIndex, subOrder.TargetUnit.GUID, subOrder.TargetUnit.DisplayName)); foreach (Weapon w in subOrder.Weapons) { AIUtil.LogAI(string.Format(" Weapon {0}", w.Name)); } } List <string> targetGUIDs = new List <string>(); foreach (AttackOrderInfo subOrder in order.SubTargetOrders) { string thisGUID = subOrder.TargetUnit.GUID; if (targetGUIDs.IndexOf(thisGUID) != -1) { // found duplicated target GUIDs AIUtil.LogAI("Multiattack error: Duplicated target GUIDs", unit); return(false); } foreach (Weapon w in subOrder.Weapons) { if (!w.CanFire) { AIUtil.LogAI("Multiattack error: weapon that cannot fire", unit); return(false); } ICombatant target = subOrder.TargetUnit; if (!unit.Combat.LOFCache.UnitHasLOFToTargetAtTargetPosition( unit, target, w.MaxRange, unit.CurrentPosition, unit.CurrentRotation, target.CurrentPosition, target.CurrentRotation, w.IndirectFireCapable)) { AIUtil.LogAI("Multiattack error: weapon that cannot fire", unit); return(false); } } } AIUtil.LogAI("Multiattack validates OK", unit); return(true); }
/// <summary> /// Attempt to kill the primary target. /// If there are not any weapons left over, we're done. /// Use the leftover weapons to try to kill secondary targets. /// If there are weapons left over and no kills to be had, assign one to each evasive target. /// If there are still weapons remaining, distribute randomly. /// </summary> /// <param name="unit"></param> /// <param name="evaluatedAttack"></param> /// <param name="primaryTargetIndex"></param> /// <returns>multi attack order, if possible, or null if a multi-attack doesn't make sense or is not possible</returns> public static MultiTargetAttackOrderInfo MakeMultiAttackOrder(AbstractActor unit, AttackEvaluator.AttackEvaluation evaluatedAttack, int primaryTargetIndex) { if ((unit.MaxTargets <= 1) || (evaluatedAttack.AttackType != AIUtil.AttackType.Shooting)) { // cannot multi-attack return(null); } ICombatant primaryTarget = unit.BehaviorTree.enemyUnits[primaryTargetIndex]; /// indices into unit.BehaviorTree.enemyUnits for secondary targets. List <int> potentialSecondaryTargetIndices = new List <int>(); Dictionary <string, bool> attackGeneratedForTargetGUID = new Dictionary <string, bool>(); for (int i = 0; i < unit.BehaviorTree.enemyUnits.Count; ++i) { ICombatant target = unit.BehaviorTree.enemyUnits[i]; bool isPrimary = (target.GUID == primaryTarget.GUID); attackGeneratedForTargetGUID[target.GUID] = isPrimary; if (isPrimary || (target.IsDead) || unit.VisibilityToTargetUnit(target) != VisibilityLevel.LOSFull) { continue; } // make sure not to permit duplicate targets for (int dupIndex = 0; dupIndex < i; ++dupIndex) { if (unit.BehaviorTree.enemyUnits[dupIndex].GUID == target.GUID) { continue; } } potentialSecondaryTargetIndices.Add(i); } if (potentialSecondaryTargetIndices.Count == 0) { // no other targets available, fall back to doing a single attack return(null); } float overkillThresholdPercentage = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_MultiTargetOverkillThreshold).FloatVal; float overkillThresholdFrac = overkillThresholdPercentage / 100.0f; List <Weapon> weaponsToKillPrimaryTarget = PartitionWeaponListToKillTarget(unit, evaluatedAttack.WeaponList, primaryTarget, overkillThresholdFrac); if ((weaponsToKillPrimaryTarget == null) || (weaponsToKillPrimaryTarget.Count == 0)) { // can't kill primary target, so fall back to single attack. return(null); } List <Weapon> weaponsSetAside = ListRemainder(evaluatedAttack.WeaponList, weaponsToKillPrimaryTarget); if ((weaponsSetAside == null) || (weaponsSetAside.Count == 0)) { // can exactly kill the primary target with a single attack, don't bother multiattacking. return(null); } Dictionary <string, List <Weapon> > weaponListsByTargetGUID = new Dictionary <string, List <Weapon> >(); for (int i = 0; i < unit.BehaviorTree.enemyUnits.Count; ++i) { ICombatant target = unit.BehaviorTree.enemyUnits[i]; weaponListsByTargetGUID[target.GUID] = new List <Weapon>(); } // Do the initial allocation to the first enemy unit weaponListsByTargetGUID[primaryTarget.GUID] = weaponsToKillPrimaryTarget; // Now, walk through the potential secondary targets and see if the set aside weapons could kill any of them. // If we find a kill, we want to set aside the weapons necessary for that kill. // if we find multiple kills, pick one, set aside those weapons, and solve again // with the remaining weapons and repeat until we can't find any more kills, or // we use up our number of attacks. // HACK optimization - only go through the list once, which biases our attacks // to prefer index #0. int usedAttacks = 1; for (int secondaryUnitDoubleIndex = 0; secondaryUnitDoubleIndex < potentialSecondaryTargetIndices.Count; ++secondaryUnitDoubleIndex) { if ((usedAttacks == unit.MaxTargets) || (weaponsSetAside.Count == 0)) { break; } int secondaryUnitActualIndex = potentialSecondaryTargetIndices[secondaryUnitDoubleIndex]; ICombatant secondaryUnit = unit.BehaviorTree.enemyUnits[secondaryUnitActualIndex]; if (attackGeneratedForTargetGUID[secondaryUnit.GUID]) { continue; } List <Weapon> weaponsToKillSecondaryTarget = PartitionWeaponListToKillTarget(unit, weaponsSetAside, secondaryUnit, overkillThresholdFrac); if ((weaponsToKillSecondaryTarget == null) || (weaponsToKillSecondaryTarget.Count == 0)) { continue; } // we've found a potential kill weaponsSetAside = ListRemainder(weaponsSetAside, weaponsToKillSecondaryTarget); weaponListsByTargetGUID[secondaryUnit.GUID] = weaponsToKillSecondaryTarget; ++usedAttacks; attackGeneratedForTargetGUID[secondaryUnit.GUID] = true; } // Now, look for targets that have evasive pips that we could strip off. // Only use a single weapon per target for (int secondaryUnitDoubleIndex = 0; secondaryUnitDoubleIndex < potentialSecondaryTargetIndices.Count; ++secondaryUnitDoubleIndex) { int secondaryUnitActualIndex = potentialSecondaryTargetIndices[secondaryUnitDoubleIndex]; ICombatant secondaryUnit = unit.BehaviorTree.enemyUnits[secondaryUnitActualIndex]; if ((usedAttacks == unit.MaxTargets) || (weaponsSetAside.Count == 0)) { break; } if (attackGeneratedForTargetGUID[secondaryUnit.GUID]) { // we already generated an attack for this target continue; } Mech secondaryMech = secondaryUnit as Mech; if ((secondaryMech != null) && (secondaryMech.IsEvasive)) { Weapon stripWeapon = FindWeaponToHitTarget(unit, weaponsSetAside, secondaryMech); if (stripWeapon != null) { List <Weapon> stripList = new List <Weapon>(); stripList.Add(stripWeapon); weaponListsByTargetGUID[secondaryUnit.GUID] = stripList; weaponsSetAside = ListRemainder(weaponsSetAside, stripList); ++usedAttacks; attackGeneratedForTargetGUID[secondaryUnit.GUID] = true; } } } // Now, if we've got extra weapons, let's send them to target 0. if (weaponsSetAside.Count > 0) { weaponListsByTargetGUID[primaryTarget.GUID].AddRange(weaponsSetAside); weaponsSetAside.Clear(); } int targetCount = 0; foreach (string guid in weaponListsByTargetGUID.Keys) { if (attackGeneratedForTargetGUID[guid]) { ++targetCount; Debug.Assert(weaponListsByTargetGUID[guid].Count > 0); } } // if, after all of that, we didn't end up having more than one target, go home and just do a single attack. if (targetCount <= 1) { return(null); } MultiTargetAttackOrderInfo multiTargetOrder = new MultiTargetAttackOrderInfo(); foreach (string guid in weaponListsByTargetGUID.Keys) { if (!attackGeneratedForTargetGUID[guid]) { continue; } ICombatant target = null; for (int i = 0; i < unit.BehaviorTree.enemyUnits.Count; ++i) { ICombatant testTarget = unit.BehaviorTree.enemyUnits[i]; if (testTarget.GUID == guid) { target = testTarget; break; } } Debug.Assert(target != null); if (target == null) { continue; } AttackOrderInfo auxAttackOrder = new AttackOrderInfo(target); for (int wi = 0; wi < weaponListsByTargetGUID[guid].Count; ++wi) { auxAttackOrder.AddWeapon(weaponListsByTargetGUID[guid][wi]); } multiTargetOrder.AddAttack(auxAttackOrder); } if (!ValidateMultiAttackOrder(multiTargetOrder, unit)) { return(null); } return(multiTargetOrder); }