diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index e45586a30b..8559bcca92 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -43,6 +43,7 @@ namespace OpenRA.Mods.Common.AI public class UnitCategories { public readonly HashSet Mcv = new HashSet(); + public readonly HashSet NavalUnits = new HashSet(); public readonly HashSet ExcludeFromSquads = new HashSet(); } @@ -837,6 +838,14 @@ namespace OpenRA.Mods.Common.AI air.Units.Add(a); } + else if (Info.UnitsCommonNames.NavalUnits.Contains(a.Info.Name)) + { + var ships = GetSquadOfType(SquadType.Naval); + if (ships == null) + ships = RegisterNewSquad(SquadType.Naval); + + ships.Units.Add(a); + } activeUnits.Add(a); } diff --git a/OpenRA.Mods.Common/AI/Squad.cs b/OpenRA.Mods.Common/AI/Squad.cs index e06fb7b3cb..ab7cc27195 100644 --- a/OpenRA.Mods.Common/AI/Squad.cs +++ b/OpenRA.Mods.Common/AI/Squad.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.AI { - public enum SquadType { Assault, Air, Rush, Protection } + public enum SquadType { Assault, Air, Rush, Protection, Naval } public class Squad { @@ -54,6 +54,9 @@ namespace OpenRA.Mods.Common.AI case SquadType.Protection: FuzzyStateMachine.ChangeState(this, new UnitsForProtectionIdleState(), true); break; + case SquadType.Naval: + FuzzyStateMachine.ChangeState(this, new NavyUnitsIdleState(), true); + break; } } diff --git a/OpenRA.Mods.Common/AI/States/NavyStates.cs b/OpenRA.Mods.Common/AI/States/NavyStates.cs new file mode 100644 index 0000000000..604a657759 --- /dev/null +++ b/OpenRA.Mods.Common/AI/States/NavyStates.cs @@ -0,0 +1,201 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) + * 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.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.AI +{ + abstract class NavyStateBase : StateBase + { + protected virtual bool ShouldFlee(Squad owner) + { + return base.ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); + } + + protected Actor FindClosestEnemy(Squad owner) + { + var first = owner.Units.First(); + + // Navy squad AI can exploit enemy naval production to find path, if any. + // (Way better than finding a nearest target which is likely to be on Ground) + // You might be tempted to move these lookups into Activate() but that causes null reference exception. + var domainIndex = first.World.WorldActor.Trait(); + var mobileInfo = first.Info.TraitInfo(); + var passable = (uint)mobileInfo.GetMovementClass(first.World.Map.Rules.TileSet); + + var navalProductions = owner.World.ActorsHavingTrait().Where(a + => owner.Bot.Info.BuildingCommonNames.NavalProduction.Contains(a.Info.Name) + && domainIndex.IsPassable(first.Location, a.Location, mobileInfo, passable) + && a.AppearsHostileTo(first)); + + if (navalProductions.Any()) + { + var nearest = navalProductions.ClosestTo(first); + + // Return nearest when it is FAR enough. + // If the naval production is within MaxBaseRadius, it implies that + // this squad is close to enemy territory and they should expect a naval combat; + // closest enemy makes more sense in that case. + if ((nearest.Location - first.Location).LengthSquared > owner.Bot.Info.MaxBaseRadius * owner.Bot.Info.MaxBaseRadius) + return nearest; + } + + return owner.Bot.FindClosestEnemy(first.CenterPosition); + } + } + + class NavyUnitsIdleState : NavyStateBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (!owner.IsTargetValid) + { + var closestEnemy = FindClosestEnemy(owner); + if (closestEnemy == null) + return; + + owner.TargetActor = closestEnemy; + } + + var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(10)) + .Where(unit => owner.Bot.Player.Stances[unit.Owner] == Stance.Enemy).ToList(); + + if (!enemyUnits.Any()) + return; + + if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) + { + foreach (var u in owner.Units) + owner.Bot.QueueOrder(new Order("AttackMove", u, false) { TargetLocation = owner.TargetActor.Location }); + + // We have gathered sufficient units. Attack the nearest enemy unit. + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), true); + } + else + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); + } + + public void Deactivate(Squad owner) { } + } + + class NavyUnitsAttackMoveState : NavyStateBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (!owner.IsTargetValid) + { + var closestEnemy = FindClosestEnemy(owner); + if (closestEnemy != null) + owner.TargetActor = closestEnemy; + else + { + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); + return; + } + } + + var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); + if (leader == null) + 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)); + foreach (var unit in owner.Units.Where(a => !ownUnits.Contains(a))) + owner.Bot.QueueOrder(new Order("AttackMove", unit, false) { TargetLocation = leader.Location }); + } + else + { + var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(12)) + .Where(a => !a.IsDead && leader.Owner.Stances[a.Owner] == Stance.Enemy && a.Info.HasTraitInfo()); + var target = enemies.ClosestTo(leader.CenterPosition); + if (target != null) + { + owner.TargetActor = target; + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackState(), true); + } + else + foreach (var a in owner.Units) + owner.Bot.QueueOrder(new Order("AttackMove", a, false) { TargetLocation = owner.TargetActor.Location }); + } + + if (ShouldFlee(owner)) + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); + } + + public void Deactivate(Squad owner) { } + } + + class NavyUnitsAttackState : NavyStateBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + if (!owner.IsTargetValid) + { + var closestEnemy = FindClosestEnemy(owner); + if (closestEnemy != null) + owner.TargetActor = closestEnemy; + else + { + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); + return; + } + } + + foreach (var a in owner.Units) + if (!BusyAttack(a)) + owner.Bot.QueueOrder(new Order("Attack", a, false) { TargetActor = owner.TargetActor }); + + if (ShouldFlee(owner)) + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); + } + + public void Deactivate(Squad owner) { } + } + + class NavyUnitsFleeState : NavyStateBase, IState + { + public void Activate(Squad owner) { } + + public void Tick(Squad owner) + { + if (!owner.IsValid) + return; + + GoToRandomOwnBuilding(owner); + owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); + } + + public void Deactivate(Squad owner) { owner.Units.Clear(); } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index cc418448e5..51876aa248 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -127,6 +127,7 @@ + diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index cbafb8417b..d936aa3abf 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -13,6 +13,7 @@ Player: Silo: silo UnitsCommonNames: Mcv: mcv + NavalUnits: ss,msub,dd,ca,lst,pt BuildingLimits: proc: 4 barr: 1 @@ -128,6 +129,7 @@ Player: Silo: silo UnitsCommonNames: Mcv: mcv + NavalUnits: ss,msub,dd,ca,lst,pt BuildingLimits: proc: 4 barr: 1 @@ -260,6 +262,7 @@ Player: Silo: silo UnitsCommonNames: Mcv: mcv + NavalUnits: ss,msub,dd,ca,lst,pt BuildingLimits: proc: 4 barr: 1 @@ -391,6 +394,7 @@ Player: Silo: silo UnitsCommonNames: Mcv: mcv + NavalUnits: ss,msub,dd,ca,lst,pt BuildingLimits: proc: 4 dome: 1