protected override BehaviorTreeResults Tick() { if (this.unit.Pathing == null) { return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(false)); } this.tickCount++; if (this.unit.Pathing.ArePathGridsComplete) { Main.LogDebug($"[AI] [BlockUntilPathfindingReadyNode] Block until pathfinding completing with grids complete"); base.LogAI("Block until pathfinding completing with grids complete", "AI.BehaviorNodes"); return(new BehaviorTreeResults(BehaviorNodeState.Success)); } float num = Time.realtimeSinceStartup - this.startTime; if (num > 60f && this.tickCount > 20) { Main.LogDebug($"[AI] [BlockUntilPathfindingReadyNode] Block until pathfinding failing, having timed out with too long a time '{num}' '{this.tickCount}'"); base.LogAI(string.Format("Block until pathfinding failing, having timed out with too long a time {0} {1}", num, this.tickCount), "AI.BehaviorNodes"); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } Main.LogDebug($"[AI] [BlockUntilPathfindingReadyNode] Block until pathfinding waiting for pathing"); base.LogAI("Block until pathfinding waiting for pathing", "AI.BehaviorNodes"); return(new BehaviorTreeResults(BehaviorNodeState.Running)); }
public static void Postfix(AbstractActor unit, bool isStationary, BehaviorTreeResults __result) { CustomAmmoCategoriesLog.Log.LogWrite("Choose result for " + unit.DisplayName + "\n"); try { if (__result.nodeState == BehaviorNodeState.Failure) { CustomAmmoCategoriesLog.Log.LogWrite(" AI choosed not attack\n"); } else if (__result.orderInfo is AttackOrderInfo) { CustomAmmoCategoriesLog.Log.LogWrite(" AI choosed to attack " + (__result.orderInfo as AttackOrderInfo).TargetUnit.DisplayName + "\n"); CustomAmmoCategories.ChooseBestWeaponForTarget(unit, (__result.orderInfo as AttackOrderInfo).TargetUnit, isStationary); } else { CustomAmmoCategoriesLog.Log.LogWrite(" AI choosed something else beside attaking\n"); } return; } catch (Exception e) { CustomAmmoCategoriesLog.Log.LogWrite("Exception " + e.ToString() + "\nFallback to default\n"); return; } }
// 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."); } }
// Duplication of HBS code, avoiding prefix=true for now. public static void Postfix(ref BehaviorTreeResults __result, string ___name, BehaviorTree ___tree, AbstractActor ___unit) { Mod.Log.Info?.Write("CJMCN:T - entered"); Mech mech = ___unit as Mech; if (mech != null && mech.WorkingJumpjets > 0) { string stayInsideRegionGUID = RegionUtil.GetStayInsideRegionGUID(___unit); float acceptableHeat = AIUtil.GetAcceptableHeatLevelForMech(mech); float currentHeat = (float)mech.CurrentHeat; Mod.Log.Info?.Write($"CJMCN:T - === actor:{CombatantUtils.Label(mech)} has currentHeat:{currentHeat} and acceptableHeat:{acceptableHeat}"); List <PathNode> sampledPathNodes = ___unit.JumpPathing.GetSampledPathNodes(); Mod.Log.Info?.Write($"CJMCN:T - calculating {sampledPathNodes.Count} nodes"); for (int i = 0; i < sampledPathNodes.Count; i++) { Vector3 candidatePos = sampledPathNodes[i].Position; float distanceBetween2D = AIUtil.Get2DDistanceBetweenVector3s(candidatePos, ___unit.CurrentPosition); float distanceBetween3D = Vector3.Distance(candidatePos, ___unit.CurrentPosition); Mod.Log.Info?.Write($"CJMCN:T - calculated distances 2D:'{distanceBetween2D}' 3D:'{distanceBetween3D} "); if (distanceBetween2D >= 1f) { float magnitude = (candidatePos - ___unit.CurrentPosition).magnitude; float jumpHeat = (float)mech.CalcJumpHeat(magnitude); Mod.Log.Info?.Write($"CJMCN:T - calculated jumpHeat:'{jumpHeat}' from magnitude:'{magnitude}. "); Mod.Log.Info?.Write($"CJMCN:T - comparing heat: [jumpHeat:'{jumpHeat}' + currentHeat:'{currentHeat}'] <= acceptableHeat:'{acceptableHeat}. "); if (jumpHeat + (float)mech.CurrentHeat <= acceptableHeat) { if (stayInsideRegionGUID != null) { MapTerrainDataCell cellAt = ___unit.Combat.MapMetaData.GetCellAt(candidatePos); if (cellAt != null) { MapEncounterLayerDataCell mapEncounterLayerDataCell = cellAt.MapEncounterLayerDataCell; if (mapEncounterLayerDataCell != null && mapEncounterLayerDataCell.regionGuidList != null && !mapEncounterLayerDataCell.regionGuidList.Contains(stayInsideRegionGUID)) { // Skip this loop iteration if Mod.Log.Info?.Write($"CJMCN:T - candidate outside of constraint region, ignoring."); goto CANDIDATE_OUTSIDE_REGION; } } } Mod.Log.Info?.Write($"CJMCN:T - adding candidate position:{candidatePos}"); ___tree.movementCandidateLocations.Add(new MoveDestination(sampledPathNodes[i], MoveType.Jumping)); } } CANDIDATE_OUTSIDE_REGION :; } } // Should already be set by prefix method //__result = BehaviorTreeResults(BehaviorNodeState.Success); }
protected override BehaviorTreeResults Tick() { // don't have lance if (unit.lance == null) { return(new BehaviorTreeResults(BehaviorNodeState.Success)); } var lanceUnits = unit.lance.unitGuids .Select(guid => unit.Combat.FindActorByGUID(guid)) .Where(u => !u.IsDead) .ToArray(); // solo in lance if (lanceUnits.Length <= 1) { return(new BehaviorTreeResults(BehaviorNodeState.Success)); } var minSpeed = lanceUnits.Min(u => u.MovementCaps.MaxWalkDistance); // ReSharper disable once CompareOfFloatsByEqualityOperator var slowestUnit = lanceUnits.First(u => minSpeed == u.MovementCaps.MaxWalkDistance); return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(unit == slowestUnit)); }
public static void Postfix(BehaviorNode __instance, ref BehaviorTreeResults __result) { if (__instance is LeafBehaviorNode && __result.orderInfo != null) { AIPause.PausePopup.AppendText(__instance.GetName()); } }
public static void Postfix(object __instance, ref BehaviorTreeResults __result, AbstractActor ___unit) { if (__result.nodeState != BehaviorNodeState.Success || !Main.Settings.ShouldPauseAI) { return; } AIPause.InfluenceMapVisual.OnInfluenceMapSort(___unit); }
static void Postfix(BehaviorNode __instance, ref BehaviorTreeResults __result) { Traverse unitT = Traverse.Create(__instance).Field("unit"); AbstractActor unit = unitT.GetValue <AbstractActor>(); if (unit is Mech mech) { float heatCheck = mech.HeatCheckMod(Mod.Config.SkillChecks.ModPerPointOfGuts); int futureHeat = mech.CurrentHeat - mech.AdjustedHeatsinkCapacity; // Check to see if we will shutdown bool passedStartupCheck = CheckHelper.DidCheckPassThreshold(Mod.Config.Heat.Shutdown, futureHeat, mech, heatCheck, ModText.FT_Check_Startup); Mod.Log.Info?.Write($"AI unit {CombatantUtils.Label(mech)} heatCheck: {heatCheck} vs. futureHeat: {futureHeat} " + $"(from currentHeat: {mech.CurrentHeat} - sinking: {mech.AdjustedHeatsinkCapacity}) => passed: {passedStartupCheck}"); if (!passedStartupCheck) { Mod.Log.Info?.Write($" -- shutdown check failed, forcing it to remain shutdown."); BehaviorTreeResults newResult = new BehaviorTreeResults(BehaviorNodeState.Failure); newResult.orderInfo = new OrderInfo(OrderType.Stand); __result = newResult; bool failedInjuryCheck = CheckHelper.ResolvePilotInjuryCheck(mech, futureHeat, -1, -1, heatCheck); if (failedInjuryCheck) { Mod.Log.Info?.Write(" -- unit did not pass injury check!"); } bool failedSystemFailureCheck = CheckHelper.ResolveSystemFailureCheck(mech, futureHeat, -1, heatCheck); if (failedSystemFailureCheck) { Mod.Log.Info?.Write(" -- unit did not pass system failure check!"); } bool failedAmmoCheck = CheckHelper.ResolveRegularAmmoCheck(mech, futureHeat, -1, heatCheck); if (failedAmmoCheck) { Mod.Log.Info?.Write(" -- unit did not pass ammo explosion check!"); } bool failedVolatileAmmoCheck = CheckHelper.ResolveVolatileAmmoCheck(mech, futureHeat, -1, heatCheck); if (failedVolatileAmmoCheck) { Mod.Log.Info?.Write(" -- unit did not pass volatile ammo explosion check!"); } QuipHelper.PublishQuip(mech, Mod.LocalizedText.Quips.Startup); } else { Mod.Log.Info?.Write($" -- shutdown check passed, starting up normally."); } } }
public static bool Prefix(BehaviorNode __instance, ref BehaviorTreeResults __result, ref BehaviorTree ___tree, ref AbstractActor ___unit) { try { List <AbstractActor> allAlliesOf = ___unit.Combat.GetAllAlliesOf(___unit); AuraBubble sensors = ___unit.sensorAura(); for (int index1 = 0; index1 < ___tree.enemyUnits.Count; ++index1) { ICombatant enemyUnit = ___tree.enemyUnits[index1]; AbstractActor abstractActor = enemyUnit as AbstractActor; float magnitude = (enemyUnit.CurrentPosition - ___unit.CurrentPosition).magnitude; if (AIUtil.UnitHasVisibilityToTargetFromPosition(___unit, enemyUnit, ___unit.CurrentPosition, allAlliesOf)) { if (___unit.CanEngageTarget(enemyUnit) || ___unit.CanDFATargetFromPosition(enemyUnit, ___unit.CurrentPosition)) { __result = new BehaviorTreeResults(BehaviorNodeState.Success); return(false); } if ((double)magnitude <= (double)___unit.MaxWalkDistance) { __result = new BehaviorTreeResults(BehaviorNodeState.Success); return(false); } if (abstractActor != null && abstractActor.IsGhosted) { float num = Mathf.Lerp(___unit.MaxWalkDistance, ___unit.MaxSprintDistance, ___unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_SignalInWeapRngWhenEnemyGhostedWithinMoveDistance).FloatVal); float range = sensors.collider.radius; if ((double)Vector3.Distance(___unit.CurrentPosition, abstractActor.CurrentPosition) - (double)range >= (double)num) { continue; } } for (int index2 = 0; index2 < ___unit.Weapons.Count; ++index2) { Weapon weapon = ___unit.Weapons[index2]; if (weapon.CanFire && (double)weapon.MaxRange >= (double)magnitude) { __result = new BehaviorTreeResults(BehaviorNodeState.Success); return(false); } } } } __result = new BehaviorTreeResults(BehaviorNodeState.Failure); return(false); } catch (Exception e) { Log.Debug?.Write(e.ToString() + "\n"); __result = new BehaviorTreeResults(BehaviorNodeState.Failure); return(false); } }
override protected BehaviorTreeResults Tick() { unit.BehaviorTree.enemyUnits = new List <ICombatant>(); string[] targetTags = { "tutorial_sprint_target" }; TagSet targetTagSet = new TagSet(targetTags); List <ITaggedItem> items = unit.Combat.ItemRegistry.GetObjectsOfTypeWithTagSet(TaggedObjectType.Unit, targetTagSet); for (int i = 0; i < items.Count; ++i) { ICombatant targetUnit = items[i] as ICombatant; if ((targetUnit != null) && (targetUnit.IsOperational)) { unit.BehaviorTree.enemyUnits.Add(targetUnit); } } return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(unit.BehaviorTree.enemyUnits.Count > 0)); }
override protected BehaviorTreeResults Tick() { unit.BehaviorTree.enemyUnits = new List <ICombatant>(); List <ITaggedItem> items = unit.Combat.ItemRegistry.GetObjectsOfType(TaggedObjectType.Unit); for (int i = 0; i < items.Count; ++i) { ICombatant targetUnit = items[i] as ICombatant; if ((targetUnit != null) && (targetUnit.team.PlayerControlsTeam) && (targetUnit.IsOperational)) { unit.BehaviorTree.enemyUnits.Add(targetUnit); } } return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(unit.BehaviorTree.enemyUnits.Count > 0)); }
override protected BehaviorTreeResults Tick() { const int laserCount = 2; List <Weapon> lasers = new List <Weapon>(); for (int wi = 0; wi < unit.Weapons.Count; ++wi) { if (lasers.Count >= laserCount) { break; } Weapon w = unit.Weapons[wi]; // TODO? (dlecompte) make this more specific if (w.WeaponCategoryValue.IsEnergy) { lasers.Add(w); } } if ((lasers.Count == 0) || (unit.BehaviorTree.enemyUnits.Count == 0)) { return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(false)); } AttackOrderInfo attackOrder = new AttackOrderInfo(unit.BehaviorTree.enemyUnits[0]); attackOrder.Weapons = lasers; attackOrder.TargetUnit = unit.BehaviorTree.enemyUnits[0]; BehaviorTreeResults results = new BehaviorTreeResults(BehaviorNodeState.Success); results.orderInfo = attackOrder; return(results); }
override protected BehaviorTreeResults Tick() { BattleTech.Designed.EncounterBoundaryChunkGameLogic boundaryChunk = unit.Combat.EncounterLayerData.encounterBoundaryChunk; if (boundaryChunk.IsInEncounterBounds(unit.CurrentPosition)) { return(new BehaviorTreeResults(BehaviorNodeState.Success)); } // find closest center float bestDist = float.MaxValue; Vector3 destination = Vector3.zero; if (boundaryChunk.encounterBoundaryRectList.Count == 0) { return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } for (int i = 0; i < boundaryChunk.encounterBoundaryRectList.Count; ++i) { RectHolder rh = boundaryChunk.encounterBoundaryRectList[i]; Vector3 c = rh.rect.center; float dist = (unit.CurrentPosition - c).magnitude; if (dist < bestDist) { bestDist = dist; destination = c; } } if ((destination - unit.CurrentPosition).magnitude < 1) { // already close (should probably have been caught, above) return(new BehaviorTreeResults(BehaviorNodeState.Success)); } unit.Pathing.UpdateAIPath(destination, destination, MoveType.Sprinting); Vector3 destinationThisTurn = unit.Pathing.ResultDestination; float movementBudget = unit.Pathing.MaxCost; PathNodeGrid grid = unit.Pathing.CurrentGrid; Vector3 successorPoint = destination; if ((grid.GetValidPathNodeAt(destinationThisTurn, movementBudget) == null) || ((destinationThisTurn - destination).magnitude > 1.0f)) { // can't get all the way to the destination. if (unit.Combat.EncounterLayerData.inclineMeshData != null) { List <AbstractActor> lanceUnits = AIUtil.GetLanceUnits(unit.Combat, unit.LanceId); List <Vector3> path = DynamicLongRangePathfinder.GetDynamicPathToDestination(destinationThisTurn, movementBudget, unit, true, lanceUnits, unit.Pathing.CurrentGrid, 100.0f); if ((path != null) && (path.Count > 0)) { destinationThisTurn = path[path.Count - 1]; } } } Vector3 cur = unit.CurrentPosition; AIUtil.LogAI(string.Format("issuing order from [{0} {1} {2}] to [{3} {4} {5}] looking at [{6} {7} {8}]", cur.x, cur.y, cur.z, destinationThisTurn.x, destinationThisTurn.y, destinationThisTurn.z, successorPoint.x, successorPoint.y, successorPoint.z )); BehaviorTreeResults results = new BehaviorTreeResults(BehaviorNodeState.Success); MovementOrderInfo mvtOrderInfo = new MovementOrderInfo(destinationThisTurn, successorPoint); mvtOrderInfo.IsSprinting = true; results.orderInfo = mvtOrderInfo; results.debugOrderString = string.Format("{0}: dest:{1} sprint:{2}", this.name, destination, mvtOrderInfo.IsSprinting); return(results); }
override protected BehaviorTreeResults Tick() { return(BehaviorTreeResults.BehaviorTreeResultsFromBoolean(unit.Combat.EncounterLayerData.IsInEncounterBounds(unit.CurrentPosition))); }
override protected BehaviorTreeResults Tick() { string regionGUID = RegionUtil.GetStayInsideRegionGUID(unit); if (regionGUID == null) { return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } if (unit.IsInRegion(regionGUID)) { return(new BehaviorTreeResults(BehaviorNodeState.Success)); } ITaggedItem item = unit.Combat.ItemRegistry.GetItemByGUID(regionGUID); if (item == null) { Debug.Log("no item with GUID: " + regionGUID); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } RegionGameLogic region = item as RegionGameLogic; if (region == null) { Debug.Log("item is not region: " + regionGUID); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } // TODO: find a point inside the region, for now using the average of all vertices. int numPoints = region.regionPointList.Length; Vector3 destination = new Vector3(); for (int pointIndex = 0; pointIndex < numPoints; ++pointIndex) { destination += region.regionPointList[pointIndex].Position; } if (numPoints == 0) { Debug.Log("no points in region: " + regionGUID); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } destination = RoutingUtil.Decrowd(destination * 1.0f / numPoints, unit); destination = RegionUtil.MaybeClipMovementDestinationToStayInsideRegion(unit, destination); var cell = unit.Combat.MapMetaData.GetCellAt(destination); destination.y = cell.cachedHeight; if ((destination - unit.CurrentPosition).magnitude < 1) { // already close (should probably have been caught, above) return(new BehaviorTreeResults(BehaviorNodeState.Success)); } bool shouldSprint = unit.CanSprint; //float sprintRange = Mathf.Max(unit.MaxSprintDistance, unit.MaxWalkDistance); float moveRange = unit.MaxWalkDistance; if ((destination - unit.CurrentPosition).magnitude < moveRange) { shouldSprint = false; } if (shouldSprint) { unit.Pathing.SetSprinting(); } else { unit.Pathing.SetWalking(); } unit.Pathing.UpdateAIPath(destination, destination, shouldSprint ? MoveType.Sprinting : MoveType.Walking); Vector3 destinationThisTurn = unit.Pathing.ResultDestination; float movementBudget = unit.Pathing.MaxCost; PathNodeGrid grid = unit.Pathing.CurrentGrid; Vector3 successorPoint = destination; var longRangeToShorRangeDistanceThreshold = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_LongRangeToShortRangeDistanceThreshold).FloatVal; if (grid.GetValidPathNodeAt(destinationThisTurn, movementBudget) == null || (destinationThisTurn - destination).magnitude > longRangeToShorRangeDistanceThreshold) { List <AbstractActor> lanceUnits = AIUtil.GetLanceUnits(unit.Combat, unit.LanceId); List <Vector3> path = DynamicLongRangePathfinder.GetDynamicPathToDestination(destination, movementBudget, unit, shouldSprint, lanceUnits, grid, 0); if (path == null || path.Count == 0) { return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } destinationThisTurn = path[path.Count - 1]; Vector2 flatDestination = new Vector2(destination.x, destination.z); float currentClosestPointInRegionDistance = float.MaxValue; Vector3?closestPoint = null; for (int i = 0; i < path.Count; ++i) { Vector3 pointOnPath = path[i]; if (RegionUtil.PointInRegion(unit.Combat, pointOnPath, regionGUID)) { var distance = (flatDestination - new Vector2(pointOnPath.x, pointOnPath.z)).sqrMagnitude; if (distance < currentClosestPointInRegionDistance) { currentClosestPointInRegionDistance = distance; closestPoint = pointOnPath; } } } if (closestPoint != null) { destinationThisTurn = closestPoint.Value; } } Vector3 cur = unit.CurrentPosition; AIUtil.LogAI(string.Format("issuing order from [{0} {1} {2}] to [{3} {4} {5}] looking at [{6} {7} {8}]", cur.x, cur.y, cur.z, destinationThisTurn.x, destinationThisTurn.y, destinationThisTurn.z, successorPoint.x, successorPoint.y, successorPoint.z )); BehaviorTreeResults results = new BehaviorTreeResults(BehaviorNodeState.Success); MovementOrderInfo mvtOrderInfo = new MovementOrderInfo(destinationThisTurn, successorPoint); mvtOrderInfo.IsSprinting = shouldSprint; results.orderInfo = mvtOrderInfo; results.debugOrderString = string.Format("{0}: dest:{1} sprint:{2}", this.name, destination, mvtOrderInfo.IsSprinting); return(results); }
// WARNING: Replaces the existing logic // isStationary here represents the attacker, not the target public static bool Prefix(AbstractActor unit, bool isStationary, ref BehaviorTreeResults __result) { // If there is no unit, exit immediately if (unit == null) { __result = new BehaviorTreeResults(BehaviorNodeState.Failure); return(false); } // If there are no enemies, exit immediately if (unit.BehaviorTree.enemyUnits.Count == 0) { Mod.Log.Info?.Write("No important enemy units, skipping decision making."); __result = new BehaviorTreeResults(BehaviorNodeState.Failure); return(false); } // Initialize decision data caches AEHelper.InitializeAttackOrderDecisionData(unit); Mod.Log.Debug?.Write($" == Evaluating attack from unit: {CombatantUtils.Label(unit)} at pos: {unit.CurrentPosition} against {unit.BehaviorTree.enemyUnits.Count} enemies."); BehaviorTreeResults behaviorTreeResults = null; AbstractActor designatedTarget = AEHelper.FilterEnemyUnitsToDesignatedTarget(unit.team as AITeam, unit.lance, unit.BehaviorTree.enemyUnits); float desTargDamage = 0f; float desTargFirepowerReduction = 0f; if (designatedTarget != null) { desTargDamage = AOHelper.MakeAttackOrderForTarget(unit, designatedTarget, isStationary, out behaviorTreeResults); desTargFirepowerReduction = AIAttackEvaluator.EvaluateFirepowerReductionFromAttack(unit, unit.CurrentPosition, designatedTarget, designatedTarget.CurrentPosition, designatedTarget.CurrentRotation, unit.Weapons, MeleeAttackType.NotSet); Mod.Log.Debug?.Write($" DesignatedTarget: {CombatantUtils.Label(designatedTarget)} will suffer: {desTargDamage} damage and lose: {desTargFirepowerReduction} firepower from attack."); } else { Mod.Log.Debug?.Write(" No designated target identified."); } float behavior1 = AIHelper.GetBehaviorVariableValue(unit.BehaviorTree, BehaviorVariableName.Float_OpportunityFireExceedsDesignatedTargetByPercentage).FloatVal; float opportunityFireThreshold = 1f + (behavior1 / 100f); float behavior2 = AIHelper.GetBehaviorVariableValue(unit.BehaviorTree, BehaviorVariableName.Float_OpportunityFireExceedsDesignatedTargetFirepowerTakeawayByPercentage).FloatVal; float opportunityFireTakeawayThreshold = 1f + (behavior2 / 100f); Mod.Log.Info?.Write($" Opportunity Fire damageThreshold: {opportunityFireThreshold} takeawayThreshold: {opportunityFireTakeawayThreshold}"); // Walk through every alive enemy, and see if a better shot presents itself. for (int j = 0; j < unit.BehaviorTree.enemyUnits.Count; j++) { ICombatant combatant = unit.BehaviorTree.enemyUnits[j]; if (combatant == designatedTarget || combatant.IsDead) { continue; } Mod.Log.Debug?.Write($" Checking opportunity fire against target: {CombatantUtils.Label(combatant)}"); AbstractActor opportunityFireTarget = combatant as AbstractActor; BehaviorTreeResults oppTargAttackOrder; // Should MAOFT take a param for opportunity attacks to simplify? float oppTargDamage = AOHelper.MakeAttackOrderForTarget(unit, combatant, isStationary, out oppTargAttackOrder); float oppTargFirepowerReduction = AIAttackEvaluator.EvaluateFirepowerReductionFromAttack(unit, unit.CurrentPosition, combatant, combatant.CurrentPosition, combatant.CurrentRotation, unit.Weapons, MeleeAttackType.NotSet); Mod.Log.Debug?.Write($" Target will suffer: {oppTargDamage} with firepower reduction: {oppTargFirepowerReduction}"); // TODO: Was where opportunity cost from evasion strip was added to target damage. // Reintroduce utility damage to this calculation bool exceedsOpportunityFireThreshold = oppTargDamage > desTargDamage * opportunityFireThreshold; Mod.Log.Debug?.Write($" Comparing damage - opportunity: {oppTargDamage} > designated: {designatedTarget} * threshold: {opportunityFireThreshold}"); bool exceedsFirepowerReductionThreshold = oppTargFirepowerReduction > desTargFirepowerReduction * opportunityFireTakeawayThreshold; Mod.Log.Debug?.Write($" Comparing firepower reduction - opportunity: {oppTargFirepowerReduction} vs. designated: {desTargFirepowerReduction} * threshold: {1f + opportunityFireTakeawayThreshold}"); // TODO: Short circuit here - takes the first result, instead of the best result. Should we fix this? if (oppTargAttackOrder != null && oppTargAttackOrder.orderInfo != null && (exceedsOpportunityFireThreshold || exceedsFirepowerReductionThreshold)) { Mod.Log.Debug?.Write(" Taking opportunity fire attack, instead of attacking designated target."); __result = oppTargAttackOrder; return(false); } } if (behaviorTreeResults != null && behaviorTreeResults.orderInfo != null) { Mod.Log.Debug?.Write("Successfuly calculated attack order"); unit.BehaviorTree.AddMessageToDebugContext(AIDebugContext.Shoot, "attacking designated target. Success"); __result = behaviorTreeResults; return(false); } Mod.Log.Debug?.Write("Could not calculate reasonable attacks. Skipping node."); __result = new BehaviorTreeResults(BehaviorNodeState.Failure); return(false); }
public static bool Prefix(AbstractActor unit, ICombatant target, int enemyUnitIndex, bool isStationary, out BehaviorTreeResults order, ref float __result) { try { Mod.Log.Info?.Write("AE:MAOFT entered."); //ModState.RangeToTargetsAlliesCache.Clear(); __result = AOHelper.MakeAttackOrderForTarget(unit, target, isStationary, out BehaviorTreeResults innerBTR); order = innerBTR; } catch (Exception e) { Mod.Log.Error?.Write("Failed to modify AttackOrder evaluation due to error: " + e.Message); Mod.Log.Error?.Write($" Source:{e.Source} StackTrace:{e.StackTrace}"); order = null; return(true); } return(false); }
// 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); }
override protected BehaviorTreeResults Tick() { BehaviorVariableValue variableValue = tree.GetBehaviorVariableValue(destinationBVarName); if (variableValue == null) { return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } string destinationGUID = variableValue.StringVal; RoutePointGameLogic destination = DestinationUtil.FindDestinationByGUID(tree, destinationGUID); if (destination == null) { return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } float sprintDistance = Mathf.Max(unit.MaxSprintDistance, unit.MaxWalkDistance); if (waitForLance) { for (int lanceMemberIndex = 0; lanceMemberIndex < unit.lance.unitGuids.Count; ++lanceMemberIndex) { ITaggedItem item = unit.Combat.ItemRegistry.GetItemByGUID(unit.lance.unitGuids[lanceMemberIndex]); if (item == null) { continue; } AbstractActor lanceUnit = item as AbstractActor; if (lanceUnit == null) { continue; } float unitMoveDistance = Mathf.Max(lanceUnit.MaxWalkDistance, lanceUnit.MaxSprintDistance); sprintDistance = Mathf.Min(sprintDistance, unitMoveDistance); } } MoveType moveType = tree.GetBehaviorVariableValue(BehaviorVariableName.Bool_RouteShouldSprint).BoolVal ? MoveType.Sprinting : MoveType.Walking; unit.Pathing.UpdateAIPath(destination.Position, destination.Position, moveType); Vector3 offset = unit.Pathing.ResultDestination - unit.CurrentPosition; if (offset.magnitude > sprintDistance) { offset = offset.normalized * sprintDistance; } Vector3 destinationThisTurn = RoutingUtil.Decrowd(unit.CurrentPosition + offset, unit); destinationThisTurn = RegionUtil.MaybeClipMovementDestinationToStayInsideRegion(unit, destinationThisTurn); float destinationRadius = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_RouteWaypointRadius).FloatVal; List <AbstractActor> unitsToWaitFor = new List <AbstractActor>(); if (waitForLance) { if (unit.lance != null) { for (int lanceGUIDIndex = 0; lanceGUIDIndex < unit.lance.unitGuids.Count; ++lanceGUIDIndex) { string guid = unit.lance.unitGuids[lanceGUIDIndex]; ITaggedItem item = unit.Combat.ItemRegistry.GetItemByGUID(guid); if (item != null) { AbstractActor lanceUnit = item as AbstractActor; if (lanceUnit != null) { unitsToWaitFor.Add(lanceUnit); } } } } else { unitsToWaitFor.Add(unit); } } if (RoutingUtil.AllUnitsInsideRadiusOfPoint(unitsToWaitFor, destination.Position, destinationRadius)) { tree.RemoveBehaviorVariableValue(destinationBVarName); } bool isSprinting = tree.GetBehaviorVariableValue(BehaviorVariableName.Bool_RouteShouldSprint).BoolVal; unit.Pathing.UpdateAIPath(destinationThisTurn, destination.Position, isSprinting ? MoveType.Sprinting : MoveType.Walking); destinationThisTurn = unit.Pathing.ResultDestination; float movementBudget = unit.Pathing.MaxCost; PathNodeGrid grid = unit.Pathing.CurrentGrid; Vector3 successorPoint = destination.Position; if ((grid.GetValidPathNodeAt(destinationThisTurn, movementBudget) == null) || ((destinationThisTurn - destination.Position).magnitude > 1.0f)) { // can't get all the way to the destination. if (unit.Combat.EncounterLayerData.inclineMeshData != null) { float maxSteepnessRatio = Mathf.Tan(Mathf.Deg2Rad * AIUtil.GetMaxSteepnessForAllLance(unit)); List <AbstractActor> lanceUnits = AIUtil.GetLanceUnits(unit.Combat, unit.LanceId); destinationThisTurn = unit.Combat.EncounterLayerData.inclineMeshData.GetDestination( unit.CurrentPosition, destinationThisTurn, movementBudget, maxSteepnessRatio, unit, isSprinting, lanceUnits, unit.Pathing.CurrentGrid, out successorPoint); } } Vector3 cur = unit.CurrentPosition; AIUtil.LogAI(string.Format("issuing order from [{0} {1} {2}] to [{3} {4} {5}] looking at [{6} {7} {8}]", cur.x, cur.y, cur.z, destinationThisTurn.x, destinationThisTurn.y, destinationThisTurn.z, successorPoint.x, successorPoint.y, successorPoint.z )); BehaviorTreeResults results = new BehaviorTreeResults(BehaviorNodeState.Success); MovementOrderInfo mvtOrderInfo = new MovementOrderInfo(destinationThisTurn, successorPoint); mvtOrderInfo.IsSprinting = isSprinting; results.orderInfo = mvtOrderInfo; results.debugOrderString = string.Format("{0} moving toward destination: {1} dest: {2}", this.name, destinationThisTurn, destination.Position); return(results); }
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); }
public static bool Prefix(ref BehaviorTreeResults __result, AbstractActor ___unit, BehaviorNode __instance) { __result = AltTick(___unit, __instance); return(false); }
override protected BehaviorTreeResults Tick() { BehaviorTreeResults results; if (unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Bool_RouteCompleted).BoolVal) { results = new BehaviorTreeResults(BehaviorNodeState.Success); results.orderInfo = new OrderInfo(OrderType.Brace); results.debugOrderString = string.Format("{0}: bracing for end of patrol route", this.name); return(results); } bool isSprinting = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Bool_RouteShouldSprint).BoolVal; if (isSprinting && unit.CanSprint) { unit.Pathing.SetSprinting(); } else { unit.Pathing.SetWalking(); } PathNodeGrid grid = unit.Pathing.CurrentGrid; if (grid.UpdateBuild(25) > 0) { // have to wait for the grid to build. results = new BehaviorTreeResults(BehaviorNodeState.Running); return(results); } if (!unit.Pathing.ArePathGridsComplete) { // have to wait for the grid to build. results = new BehaviorTreeResults(BehaviorNodeState.Running); return(results); } float destinationRadius = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_RouteWaypointRadius).FloatVal; RouteGameLogic myPatrolRoute = getRoute(); if (myPatrolRoute == null) { AIUtil.LogAI("Move Along Route failing because no route found", unit); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } BehaviorVariableValue nrpiVal = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Int_RouteTargetPoint); int nextRoutePointIndex = (nrpiVal != null) ? nrpiVal.IntVal : 0; BehaviorVariableValue pfVal = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Bool_RouteFollowingForward); bool patrollingForward = (pfVal != null) ? pfVal.BoolVal : true; PatrolRouteWaypoints routeWaypointIterator = null; switch (myPatrolRoute.routeTransitType) { case RouteTransitType.Circuit: routeWaypointIterator = new CircuitRouteWaypoints(nextRoutePointIndex, patrollingForward, myPatrolRoute.routePointList.Length); break; case RouteTransitType.OneWay: routeWaypointIterator = new OneWayRouteWaypoints(nextRoutePointIndex, patrollingForward, myPatrolRoute.routePointList.Length); break; case RouteTransitType.PingPong: routeWaypointIterator = new PingPongRouteWaypoints(nextRoutePointIndex, patrollingForward, myPatrolRoute.routePointList.Length); break; default: Debug.LogError("Invalid route transit type: " + myPatrolRoute.routeTransitType); AIUtil.LogAI("Move Along Route failing because patrol route was set to an invalid transit type: " + myPatrolRoute.routeTransitType, unit); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } float movementAvailable = unit.Pathing.MaxCost * unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.Float_PatrolRouteThrottlePercentage).FloatVal / 100.0f; bool isComplete = false; int nextWaypoint = -1; bool nextPointGoesForward = false; Vector3 successorPoint; List <PathNode> availablePathNodes = unit.Pathing.CurrentGrid.GetSampledPathNodes(); // prune for region string regionGUID = RegionUtil.StayInsideRegionGUID(unit); if (!string.IsNullOrEmpty(regionGUID)) { availablePathNodes = availablePathNodes.FindAll(node => RegionUtil.PointInRegion(unit.Combat, node.Position, regionGUID)); } string guardGUID = unit.BehaviorTree.GetBehaviorVariableValue(BehaviorVariableName.String_GuardLanceGUID).StringVal; Lance guardLance = guardGUID != null?unit.Combat.ItemRegistry.GetItemByGUID <Lance>(guardGUID) : null; // if guarding units, adjust movement available to account for their speed if (guardLance != null) { movementAvailable = adjustMovementAvailableForGuardLance(unit, movementAvailable, guardLance); } // prune for distance from start point availablePathNodes = availablePathNodes.FindAll(node => node.CostToThisNode <= movementAvailable); // if there is a guarding lance, make sure that we're not moving out of the lance tether if (guardLance != null) { availablePathNodes = filterAvailablePathNodesForGuardTether(unit, availablePathNodes, guardLance); } Vector3 patrolPoint = getReachablePointOnRoute(unit.CurrentPosition, myPatrolRoute, routeWaypointIterator, availablePathNodes, out isComplete, out nextWaypoint, out nextPointGoesForward, out successorPoint); unit.BehaviorTree.unitBehaviorVariables.SetVariable(BehaviorVariableName.Bool_RouteFollowingForward, new BehaviorVariableValue(nextPointGoesForward)); unit.BehaviorTree.unitBehaviorVariables.SetVariable(BehaviorVariableName.Int_RouteTargetPoint, new BehaviorVariableValue(nextWaypoint)); unit.BehaviorTree.unitBehaviorVariables.SetVariable(BehaviorVariableName.Bool_RouteCompleted, new BehaviorVariableValue(isComplete)); //Vector3 destination = RegionUtil.MaybeClipMovementDestinationToStayInsideRegion(unit, patrolPoint); Vector3 destination = patrolPoint; if (!isComplete) { List <PathNode> path = constructPath(unit.Combat.HexGrid, destination, availablePathNodes); if ((path.Count == 0) || ((path.Count == 1) && (AIUtil.Get2DDistanceBetweenVector3s(path[0].Position, unit.CurrentPosition) < 1))) { // can't actually make progress - fail here, and presumably pass later on. AIUtil.LogAI("Move Along Route failing because no nodes in path.", unit); DialogueGameLogic proximityDialogue = unit.Combat.ItemRegistry.GetItemByGUID <DialogueGameLogic>(unit.Combat.Constants.CaptureEscortProximityDialogID); if (proximityDialogue != null) { TriggerDialog triggerDialogueMessage = new TriggerDialog(unit.GUID, unit.Combat.Constants.CaptureEscortProximityDialogID, async: false); unit.Combat.MessageCenter.PublishMessage(triggerDialogueMessage); } else { Debug.LogError("Could not find CaptureEscortProximityDialog. This is only a real error message if this is a Capture Escort (Normal Escort) mission. For other missions (Story, Ambush Convoy, etc) you can safely ignore this error message."); } return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } destination = path[path.Count - 1].Position; } Vector3 cur = unit.CurrentPosition; if ((destination - cur).magnitude < 1) { // can't actually make progress - fail here, and presumably pass later on. AIUtil.LogAI("Move Along Route failing because destination too close to unit start.", unit); return(new BehaviorTreeResults(BehaviorNodeState.Failure)); } AIUtil.LogAI(string.Format("issuing order from [{0} {1} {2}] to [{3} {4} {5}] looking at [{6} {7} {8}]", cur.x, cur.y, cur.z, destination.x, destination.y, destination.z, successorPoint.x, successorPoint.y, successorPoint.z ), unit); results = new BehaviorTreeResults(BehaviorNodeState.Success); MovementOrderInfo mvtOrderInfo = new MovementOrderInfo(destination, successorPoint); mvtOrderInfo.IsSprinting = isSprinting; results.orderInfo = mvtOrderInfo; results.debugOrderString = string.Format("{0}: dest:{1} sprint:{2}", this.name, destination, mvtOrderInfo.IsSprinting); return(results); }