#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System.Collections.Generic; using System.Linq; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class GroundStateBase : StateBase { protected virtual bool ShouldFlee(Squad owner) { return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); } protected static (Actor Actor, WVec Offset) FindClosestEnemy(Squad owner) { return owner.SquadManager.FindClosestEnemy(owner.Units.First()); } protected static IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) { return owner.SquadManager.FindEnemies( actors, owner.Units.First()); } protected static Actor ClosestToEnemy(Squad owner) { return SquadManagerBotModule.ClosestTo(owner.Units, owner.TargetActor); } } sealed class GroundUnitsIdleState : GroundStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) return; } var enemyUnits = FindEnemies(owner, owner.World.FindActorsInCircle(owner.Target.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius))) .Select(x => x.Actor) .ToList(); if (enemyUnits.Count == 0) return; if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) { owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); // We have gathered sufficient units. Attack the nearest enemy unit. owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), true); } else owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); } public void Deactivate(Squad owner) { } } sealed class GroundUnitsAttackMoveState : GroundStateBase, IState { int lastUpdatedTick; CPos? lastLeaderLocation; Actor lastTarget; public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } var leader = ClosestToEnemy(owner); if (leader == null) return; if (leader.Location != lastLeaderLocation) { lastLeaderLocation = leader.Location; lastUpdatedTick = owner.World.WorldTick; } if (owner.TargetActor != lastTarget) { lastTarget = owner.TargetActor; lastUpdatedTick = owner.World.WorldTick; } // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds // This works around the squad being stuck trying to attack-move to a location // that they cannot path to, generating expensive pathfinding calls each tick. if (owner.World.WorldTick > lastUpdatedTick + 63) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); return; } var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3) .Where(a => a.Owner == owner.Units.First().Owner && owner.Units.Contains(a)).ToHashSet(); if (ownUnits.Count < owner.Units.Count) { // Since units have different movement speeds, they get separated while approaching the target. // Let them regroup into tighter formation. owner.Bot.QueueOrder(new Order("Stop", leader, false)); var units = owner.Units.Where(a => !ownUnits.Contains(a)).ToArray(); owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: units)); } else { var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)); if (target.Actor != null) { owner.SetActorToTarget(target); owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState(), true); } else owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); } if (ShouldFlee(owner)) owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); } public void Deactivate(Squad owner) { } } sealed class GroundUnitsAttackState : GroundStateBase, IState { int lastUpdatedTick; CPos? lastLeaderLocation; Actor lastTarget; public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } var leader = ClosestToEnemy(owner); if (leader?.Location != lastLeaderLocation) { lastLeaderLocation = leader?.Location; lastUpdatedTick = owner.World.WorldTick; } if (owner.TargetActor != lastTarget) { lastTarget = owner.TargetActor; lastUpdatedTick = owner.World.WorldTick; } // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds // This works around the squad being stuck trying to attack-move to a location // that they cannot path to, generating expensive pathfinding calls each tick. if (owner.World.WorldTick > lastUpdatedTick + 63) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); return; } foreach (var a in owner.Units) if (!BusyAttack(a)) owner.Bot.QueueOrder(new Order("AttackMove", a, owner.Target, false)); if (ShouldFlee(owner)) owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); } public void Deactivate(Squad owner) { } } sealed class GroundUnitsFleeState : GroundStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; GoToRandomOwnBuilding(owner); owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); } public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } } }