public static AttackEvaluation Attack(TroopData troop, PlayerData oldOwner, PlayerData newOwner, int shipsOnPlanetAfterEvent, int lostShipsByOwner, int lostShipsByAttacker, int lostShipsByLanding)
    {
        AttackEvaluation evaluation = new AttackEvaluation();
        if (troop.Owner == null || troop.Owner.IsHumanPlayer) { // null-check for debugging purposes: if a ship has been sent from a neutral planet
            evaluation.Type = EvaluationType.AttackedPlanet;
            evaluation.LostShips = lostShipsByAttacker + lostShipsByLanding;
            if (newOwner == troop.Owner) {                   // Captured
                if (lostShipsByLanding > 0) {
                    evaluation.Outcome = EvaluationOutcome.Neutral;     // Bitterer Beigeschmack
                } else {
                    evaluation.Outcome = EvaluationOutcome.Success;
                }
                evaluation.Importance = 80;
            } else {
                evaluation.Outcome = EvaluationOutcome.Lost;
                evaluation.Importance = 75;
            }
        } else if (oldOwner != null && oldOwner.IsHumanPlayer) {
            evaluation.Type = EvaluationType.GotAttacked;
            evaluation.LostShips = lostShipsByOwner;
            if (newOwner == troop.Owner) {                   // Captured
                evaluation.Outcome = EvaluationOutcome.Lost;
                evaluation.Importance = 80;
            } else {
                evaluation.Outcome = EvaluationOutcome.Success;
                evaluation.Importance = 60;
            }
        } else {
            if (oldOwner != newOwner) {
                evaluation.Type = EvaluationType.CaptureViewer;
                evaluation.Importance = 15;
            } else {
                evaluation.Type = EvaluationType.AttackViewer;
                evaluation.Importance = 5;
            }
            evaluation.LostShips = lostShipsByOwner + lostShipsByAttacker + lostShipsByLanding;     // A viewer is only intersted in the total number of ships destroyed
            evaluation.Outcome = EvaluationOutcome.Neutral;
        }

        evaluation.ShipOwner = troop.Owner;
        evaluation.PlanetOwner = newOwner;
        evaluation.IncomingShips = troop.ShipCount;
        evaluation.ShipsOnPlanetAfterEvent = shipsOnPlanetAfterEvent;

        evaluation.Planet = troop.TargetPlanet;
        evaluation.OriginalOwner = oldOwner;

        Debug.Log("An attack of " + evaluation.IncomingShips
            + " ships reached planet " + evaluation.Planet.Name
            + " - Losses:  owner = " + lostShipsByOwner
            + ", attacker = " + (lostShipsByAttacker + lostShipsByLanding)
            + "; New owner: " + (newOwner == null ? "neutral" : newOwner.Name)
            + "");
        return evaluation;
    }
Example #2
0
        // Evaluate all possible attacks for the attacker and target based upon their current position. Returns the total damage the target will take,
        //   which will be compared against all other targets to determine the optimal attack to make
        public static float MakeAttackOrderForTarget(AbstractActor attackerAA, ICombatant target, bool isStationary, out BehaviorTreeResults order)
        {
            Mod.Log.Debug?.Write($"Evaluating AttackOrder from ({CombatantUtils.Label(attackerAA)}) against ({CombatantUtils.Label(target)} at position: ({target.CurrentPosition})");

            // If the unit has no visibility to the target from the current position, they can't attack. Return immediately.
            if (!AIUtil.UnitHasVisibilityToTargetFromCurrentPosition(attackerAA, target))
            {
                order = BehaviorTreeResults.BehaviorTreeResultsFromBoolean(false);
                return(0f);
            }

            Mech  attackerMech   = attackerAA as Mech;
            float currentHeat    = attackerMech == null ? 0f : (float)attackerMech.CurrentHeat;
            float acceptableHeat = attackerMech == null ? float.MaxValue : AIUtil.GetAcceptableHeatLevelForMech(attackerMech);

            Mod.Log.Debug?.Write($" heat: current: {currentHeat} acceptable: {acceptableHeat}");

            //float weaponToHitThreshold = attackerAA.BehaviorTree.weaponToHitThreshold;

            // Filter weapons that cannot contribute to the battle
            CandidateWeapons candidateWeapons = new CandidateWeapons(attackerAA, target);

            Mech targetMech      = target as Mech;
            bool targetIsEvasive = targetMech != null && targetMech.IsEvasive;

            List <List <CondensedWeapon> >[] weaponSetsByAttackType =
            {
                new List <List <CondensedWeapon> >()
                {
                },
                new List <List <CondensedWeapon> >()
                {
                },
                new List <List <CondensedWeapon> >()
                {
                }
            };

            // Note: Disabled the evasion fractional checking that Vanilla uses. Should make units more free with ammunition against evasive foes
            //float evasiveToHitFraction = AIHelper.GetBehaviorVariableValue(attackerAA.BehaviorTree, BehaviorVariableName.Float_EvasiveToHitFloor).FloatVal / 100f;
            // TODO: Reappropriate BehaviorVariableName.Float_EvasiveToHitFloor as floor for all shots?

            // Build three sets of sets; ranged, melee, dfa. Each set contains a set of weapons O(n^2) here
            weaponSetsByAttackType[0] = AEHelper.MakeWeaponSets(candidateWeapons.RangedWeapons);

            // Evaluate melee attacks
            string cannotEngageInMeleeMsg = "";

            if (attackerMech == null || !attackerMech.CanEngageTarget(target, out cannotEngageInMeleeMsg))
            {
                Mod.Log.Debug?.Write($" attacker cannot melee, or cannot engage due to: '{cannotEngageInMeleeMsg}'");
            }
            else
            {
                // Check Retaliation
                // TODO: Retaliation should consider all possible attackers, not just the attacker
                // TODO: Retaliation should consider how much damage you do with melee vs. non-melee - i.e. punchbots should probably prefer punching over weak weapons fire
                // TODO: Should consider if heat would be reduced by melee attack
                if (AEHelper.MeleeDamageOutweighsRisk(attackerMech, target))
                {
                    // Generate base list
                    List <List <CondensedWeapon> > meleeWeaponSets = AEHelper.MakeWeaponSets(candidateWeapons.MeleeWeapons);

                    // Add melee weapons to each set
                    CondensedWeapon cMeleeWeapon = new CondensedWeapon(attackerMech.MeleeWeapon);
                    for (int i = 0; i < meleeWeaponSets.Count; i++)
                    {
                        meleeWeaponSets[i].Add(cMeleeWeapon);
                    }

                    weaponSetsByAttackType[1] = meleeWeaponSets;
                }
                else
                {
                    Mod.Log.Debug?.Write($" potential melee retaliation too high, skipping melee.");
                }
            }

            WeaponHelper.FilterWeapons(attackerAA, target, out List <Weapon> rangedWeps, out List <Weapon> meleeWeps, out List <Weapon> dfaWeps);

            AttackDetails attackDetails = new AttackDetails(attacker: attackerAA, target: target as AbstractActor,
                                                            attackPos: attackerAA.CurrentPosition, targetPos: target.CurrentPosition, useRevengeBonus: true);

            AttackEvaluation rangedAE = RangedCalculator.OptimizeAttack(attackDetails, rangedWeps);
            AttackEvaluation meleeAE  = MeleeCalculator.OptimizeAttack(meleeWeps, attackerAA, target);
            AttackEvaluation dfaAE    = DFACalculator.OptimizeAttack(dfaWeps, attackerAA, target);

            List <AttackEvaluation> allAttackSolutions = new List <AttackEvaluation>()
            {
                rangedAE, meleeAE, dfaAE
            };

            Mod.Log.Debug?.Write(string.Format("found {0} different attack solutions", allAttackSolutions.Count));

            // TODO: Apply mode - CleverGirlHelper.ApplyAmmoMode(wep, cWeapon.ammoAndMode);

            // Find the attack with the best damage across all attacks
            float bestRangedEDam = 0f;
            float bestMeleeEDam  = 0f;
            float bestDFAEDam    = 0f;

            for (int m = 0; m < allAttackSolutions.Count; m++)
            {
                AttackEvaluation attackEvaluation = allAttackSolutions[m];
                Mod.Log.Debug?.Write($"evaluated attack of type {attackEvaluation.AttackType} with {attackEvaluation.WeaponList.Count} weapons, " +
                                     $"damage EV of {attackEvaluation.ExpectedDamage}, heat {attackEvaluation.HeatGenerated}");
                switch (attackEvaluation.AttackType)
                {
                case AIUtil.AttackType.Shooting:
                    bestRangedEDam = Mathf.Max(bestRangedEDam, attackEvaluation.ExpectedDamage);
                    break;

                case AIUtil.AttackType.Melee:
                    bestMeleeEDam = Mathf.Max(bestMeleeEDam, attackEvaluation.ExpectedDamage);
                    break;

                case AIUtil.AttackType.DeathFromAbove:
                    bestDFAEDam = Mathf.Max(bestDFAEDam, attackEvaluation.ExpectedDamage);
                    break;

                default:
                    Debug.Log("unknown attack type: " + attackEvaluation.AttackType);
                    break;
                }
            }
            Mod.Log.Debug?.Write($"best shooting: {bestRangedEDam}  melee: {bestMeleeEDam}  dfa: {bestDFAEDam}");

            //float existingTargetDamageForOverheat = AIHelper.GetBehaviorVariableValue(attackerAA.BehaviorTree, BehaviorVariableName.Float_ExistingTargetDamageForOverheatAttack).FloatVal;

            AbstractActor   targetActor         = target as AbstractActor;
            List <PathNode> meleeDestsForTarget = attackerMech.Pathing.GetMeleeDestsForTarget(targetActor);

            // LOGIC: Now, evaluate every set of attacks in the list
            for (int n = 0; n < allAttackSolutions.Count; n++)
            {
                AttackEvaluator.AttackEvaluation attackEvaluation2 = allAttackSolutions[n];
                Mod.Log.Debug?.Write("------");
                Mod.Log.Debug?.Write($"Evaluating attack solution #{n} vs target: {CombatantUtils.Label(targetActor)}");

                // TODO: Do we really need this spam?
                StringBuilder weaponListSB = new StringBuilder();
                weaponListSB.Append(" Weapons: (");
                foreach (Weapon weapon3 in attackEvaluation2.WeaponList)
                {
                    weaponListSB.Append("'");
                    weaponListSB.Append(weapon3.Name);
                    weaponListSB.Append("', ");
                }
                weaponListSB.Append(")");
                Mod.Log.Debug?.Write(weaponListSB.ToString());

                if (attackEvaluation2.WeaponList.Count == 0)
                {
                    Mod.Log.Debug?.Write("SOLUTION REJECTED - no weapons!");
                }

                // TODO: Does heatGenerated account for jump heat?
                // TODO: Does not rollup heat!
                bool willCauseOverheat = attackEvaluation2.HeatGenerated + currentHeat > acceptableHeat;
                Mod.Log.Debug?.Write($"heat generated: {attackEvaluation2.HeatGenerated}  current: {currentHeat}  acceptable: {acceptableHeat}  willOverheat: {willCauseOverheat}");
                if (willCauseOverheat && attackerMech.OverheatWillCauseDeath())
                {
                    Mod.Log.Debug?.Write("SOLUTION REJECTED - overheat would cause own death");
                    continue;
                }
                // TODO: Check for acceptable damage from overheat - as per below
                //bool flag6 = num4 >= existingTargetDamageForOverheat;
                //Mod.Log.Debug?.Write("but enough damage for overheat attack? " + flag6);
                //bool flag7 = attackEvaluation2.lowestHitChance >= weaponToHitThreshold;
                //Mod.Log.Debug?.Write("but enough accuracy for overheat attack? " + flag7);
                //if (willCauseOverheat && (!flag6 || !flag7)) {
                //    Mod.Log.Debug?.Write("SOLUTION REJECTED - not enough damage or accuracy on an attack that will overheat");
                //    continue;
                //}

                // LOGIC: If we have some damage from an attack, can we improve upon it as a morale / called shot / multi-attack?
                if (attackEvaluation2.ExpectedDamage > 0f)
                {
                    BehaviorTreeResults behaviorTreeResults = new BehaviorTreeResults(BehaviorNodeState.Success);

                    // LOGIC: Check for a morale attack (based on available morale) - target must be shutdown or knocked down
                    //CalledShotAttackOrderInfo offensivePushAttackOrderInfo = AEHelper.MakeOffensivePushOrder(attackerAA, attackEvaluation2, target);
                    //if (offensivePushAttackOrderInfo != null) {
                    //    behaviorTreeResults.orderInfo = offensivePushAttackOrderInfo;
                    //    behaviorTreeResults.debugOrderString = attackerAA.DisplayName + " using offensive push";
                    //}

                    // LOGIC: Check for a called shot - target must be shutdown or knocked down
                    //CalledShotAttackOrderInfo calledShotAttackOrderInfo = AEHelper.MakeCalledShotOrder(attackerAA, attackEvaluation2, target, false);
                    //if (calledShotAttackOrderInfo != null) {
                    //    behaviorTreeResults.orderInfo = calledShotAttackOrderInfo;
                    //    behaviorTreeResults.debugOrderString = attackerAA.DisplayName + " using called shot";
                    //}

                    // LOGIC: Check for multi-attack that will fit within our heat boundaries
                    //MultiTargetAttackOrderInfo multiAttackOrderInfo = MultiAttack.MakeMultiAttackOrder(attackerAA, attackEvaluation2, enemyUnitIndex);
                    //if (!willCauseOverheat && multiAttackOrderInfo != null) {
                    //     Multi-attack in RT / BTA only makes sense to:
                    //      1. maximize breaching shot (which ignores cover/etc) if you a single weapon
                    //      2. spread status effects around while firing on a single target
                    //      3. maximizing total damage across N targets, while sacrificing potential damage at a specific target
                    //        3a. Especially with set sof weapons across range brackets, where you can split short-range weapons and long-range weapons
                    //    behaviorTreeResults.orderInfo = multiAttackOrderInfo;
                    //    behaviorTreeResults.debugOrderString = attackerAA.DisplayName + " using multi attack";
                    //}

                    AttackOrderInfo attackOrderInfo = new AttackOrderInfo(target)
                    {
                        Weapons    = attackEvaluation2.WeaponList,
                        TargetUnit = target
                    };
                    AIUtil.AttackType attackType = attackEvaluation2.AttackType;

                    List <PathNode> dfaDestinations = attackerAA.JumpPathing.GetDFADestsForTarget(targetActor);
                    if (attackType == AIUtil.AttackType.DeathFromAbove)
                    {
                        attackOrderInfo.IsDeathFromAbove = true;
                        attackOrderInfo.Weapons.Remove(attackerMech.MeleeWeapon);
                        attackOrderInfo.Weapons.Remove(attackerMech.DFAWeapon);
                        attackOrderInfo.AttackFromLocation = attackerMech.FindBestPositionToMeleeFrom(targetActor, dfaDestinations);
                    }
                    else if (attackType == AIUtil.AttackType.Melee)
                    {
                        attackOrderInfo.IsMelee = true;
                        attackOrderInfo.Weapons.Remove(attackerMech.MeleeWeapon);
                        attackOrderInfo.Weapons.Remove(attackerMech.DFAWeapon);

                        attackOrderInfo.AttackFromLocation = attackerMech.FindBestPositionToMeleeFrom(targetActor, meleeDestsForTarget);
                    }

                    behaviorTreeResults.orderInfo        = attackOrderInfo;
                    behaviorTreeResults.debugOrderString = $" using attack type: {attackEvaluation2.AttackType} against: {target.DisplayName}";

                    Mod.Log.Debug?.Write("attack order: " + behaviorTreeResults.debugOrderString);
                    order = behaviorTreeResults;
                    return(attackEvaluation2.ExpectedDamage);
                }
                Mod.Log.Debug?.Write("Rejecting attack for not having any expected damage");
            }

            Mod.Log.Debug?.Write("There are no targets I can shoot at without overheating.");
            order = null;
            return(0f);
        }
        // Optimization means to maximize the total damage output at this time given the weapons list.
        //   This is within the bounds of acceptable heat, self-damage, etc. and reflect the behavior/role of the attacker
        //   The output should be an AE that's immediately consumable by vanilla AI.
        //
        // This expects that weapons has no melee weapons in it, and that target is valid!
        public static AttackEvaluation OptimizeAttack(AttackDetails details, List <Weapon> weapons)
        {
            try
            {
                /*
                 * 1. Check that weapons, attacker, target is not null
                 * 2. Weapons are already checked for LoF and range
                 * 3. Build an EV and Heat ratio for each weapon
                 * 4. Evaluate heat - sum all heat values, but drop weapons step by step until below safe threshold
                 */

                Mod.Log.Debug?.Write($"Generating ranged AEs for {details.Attacker.DistinctId()} vs. {details.Target.DistinctId()}");

                // Build a list of all possible outcomes from shooting
                List <WeaponAttackEval> damageOptimized = new List <WeaponAttackEval>();
                List <WeaponAttackEval> heatOptimized   = new List <WeaponAttackEval>();
                List <WeaponAttackEval> stabOptimized   = new List <WeaponAttackEval>();
                for (int i = 0; i < weapons.Count; i++)
                {
                    Weapon weapon = weapons[i];
                    Mod.Log.Debug?.Write($"Optimizing ammoBoxes for weapon: {weapon.UIName}");
                    (WeaponAttackEval dmgWAE, WeaponAttackEval heatWAE, WeaponAttackEval stabWAE) = OptimizeAmmoPairForAttack(weapon, details);
                    damageOptimized.Add(dmgWAE);
                    heatOptimized.Add(heatWAE);
                    stabOptimized.Add(stabWAE);
                }

                // Next, choose a strategy based upon target.
                List <WeaponAttackEval> selectedAttacks = SelectAttackStrategy(details, damageOptimized, heatOptimized, stabOptimized);

                // Now, filter the attacks based upon the attacker
                selectedAttacks = FilterForHeatBudget(selectedAttacks, details);
                selectedAttacks = FilterForAmmo(selectedAttacks, details);
                selectedAttacks = FilterForBreachingShot(selectedAttacks, details);

                // Finally, build an attackEvaluation
                Mod.Log.Debug?.Write("Adding weapons to AE");
                AttackEvaluation ae = new AttackEvaluation()
                {
                    AttackType      = AIUtil.AttackType.Shooting,
                    lowestHitChance = 1,
                    WeaponList      = new List <Weapon>()
                };
                foreach (WeaponAttackEval wae in selectedAttacks)
                {
                    Mod.Log.Debug?.Write($"  -- adding {wae.Weapon.UIName} to list.");
                    ae.HeatGenerated += wae.Weapon.HeatGenerated;
                    // TODO: This doesn't weight utility damage!
                    ae.ExpectedDamage += wae.EVDirectDmg + wae.EVHeat + wae.EVStab;
                    if (wae.ToHit < ae.lowestHitChance)
                    {
                        ae.lowestHitChance = wae.ToHit;
                    }
                    ae.WeaponList.Add(wae.Weapon);
                }

                Mod.Log.Debug?.Write($"Returning AE with {ae.WeaponList.Count} weapons.");
                return(ae);
            }
            catch (Exception e)
            {
                Mod.Log.Error?.Write(e, "Failed to optimize ranged attack due to error!");
                return(new AttackEvaluation()
                {
                    AttackType = AIUtil.AttackType.Shooting
                });
            }
        }
 public static AttackEvaluation Supply(TroopData troop, int shipsOnPlanetAfterEvent, int lostShips)
 {
     AttackEvaluation evaluation = new AttackEvaluation();
     evaluation.Type = EvaluationType.Supply;
     if (lostShips > 0) {
         evaluation.Outcome = EvaluationOutcome.Lost;
         evaluation.Importance = Mathf.Lerp(50, 100, Mathf.Min(1, lostShips / 500));
     } else {
         evaluation.Outcome = EvaluationOutcome.Neutral;
         evaluation.Importance = 40;
     }
     evaluation.ShipOwner = troop.Owner;
     evaluation.PlanetOwner = troop.Owner;
     evaluation.IncomingShips = troop.ShipCount;
     evaluation.ShipsOnPlanetAfterEvent = shipsOnPlanetAfterEvent;
     evaluation.LostShips = lostShips;
     evaluation.Planet = troop.TargetPlanet;
     evaluation.OriginalOwner = troop.Owner;
     Debug.Log("A supply of "
         + evaluation.IncomingShips
         + " reached planet "
         + evaluation.Planet.Name + " - " + lostShips
         + " ships were lost due to full hangar");
     return evaluation;
 }