#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.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class AirStateBase : StateBase { static readonly BitSet AirTargetTypes = new("Air"); protected const int MissileUnitMultiplier = 3; protected static int CountAntiAirUnits(IEnumerable units) { if (!units.Any()) return 0; var missileUnitsCount = 0; foreach (var unit in units) { if (unit == null || unit.Info.HasTraitInfo()) continue; foreach (var ab in unit.TraitsImplementing()) { if (ab.IsTraitDisabled || ab.IsTraitPaused) continue; foreach (var a in ab.Armaments) { if (a.Weapon.IsValidTarget(AirTargetTypes)) { missileUnitsCount++; break; } } } } return missileUnitsCount; } protected static Actor FindDefenselessTarget(Squad owner) { FindSafePlace(owner, out var target, true); return target; } protected static CPos? FindSafePlace(Squad owner, out Actor detectedEnemyTarget, bool needTarget) { var map = owner.World.Map; var dangerRadius = owner.SquadManager.Info.DangerScanRadius; detectedEnemyTarget = null; var columnCount = (map.MapSize.X + dangerRadius - 1) / dangerRadius; var rowCount = (map.MapSize.Y + dangerRadius - 1) / dangerRadius; var checkIndices = Exts.MakeArray(columnCount * rowCount, i => i).Shuffle(owner.World.LocalRandom); foreach (var i in checkIndices) { var pos = new MPos(i % columnCount * dangerRadius + dangerRadius / 2, i / columnCount * dangerRadius + dangerRadius / 2).ToCPos(map); if (NearToPosSafely(owner, map.CenterOfCell(pos), out detectedEnemyTarget)) { if (needTarget && detectedEnemyTarget == null) continue; return pos; } } return null; } protected static bool NearToPosSafely(Squad owner, WPos loc) { return NearToPosSafely(owner, loc, out _); } protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedEnemyTarget) { detectedEnemyTarget = null; var dangerRadius = owner.SquadManager.Info.DangerScanRadius; var unitsAroundPos = owner.World.FindActorsInCircle(loc, WDist.FromCells(dangerRadius)) .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); if (unitsAroundPos.Count == 0) return true; if (CountAntiAirUnits(unitsAroundPos) * MissileUnitMultiplier < owner.Units.Count) { detectedEnemyTarget = unitsAroundPos.Random(owner.Random); return true; } return false; } // Checks the number of anti air enemies around units protected virtual bool ShouldFlee(Squad owner) { return ShouldFlee(owner, enemies => CountAntiAirUnits(enemies) * MissileUnitMultiplier > owner.Units.Count); } } class AirIdleState : AirStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; if (ShouldFlee(owner)) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } var e = FindDefenselessTarget(owner); if (e == null) return; owner.TargetActor = e; owner.FuzzyStateMachine.ChangeState(owner, new AirAttackState(), true); } public void Deactivate(Squad owner) { } } class AirAttackState : AirStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; if (!owner.IsTargetValid) { var a = owner.Units.Random(owner.Random); var closestEnemy = owner.SquadManager.FindClosestEnemy(a.CenterPosition); if (closestEnemy != null) owner.TargetActor = closestEnemy; else { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } } if (!NearToPosSafely(owner, owner.TargetActor.CenterPosition)) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } foreach (var a in owner.Units) { if (BusyAttack(a)) continue; var ammoPools = a.TraitsImplementing().ToArray(); if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault())) { if (IsRearming(a)) continue; if (!HasAmmo(ammoPools)) { owner.Bot.QueueOrder(new Order("ReturnToBase", a, false)); continue; } } if (CanAttackTarget(a, owner.TargetActor)) owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); } } public void Deactivate(Squad owner) { } } class AirFleeState : AirStateBase, IState { public void Activate(Squad owner) { } public void Tick(Squad owner) { if (!owner.IsValid) return; foreach (var a in owner.Units) { var ammoPools = a.TraitsImplementing().ToArray(); if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault()) && !FullAmmo(ammoPools)) { if (IsRearming(a)) continue; owner.Bot.QueueOrder(new Order("ReturnToBase", a, false)); continue; } owner.Bot.QueueOrder(new Order("Move", a, Target.FromCell(owner.World, RandomBuildingLocation(owner)), false)); } owner.FuzzyStateMachine.ChangeState(owner, new AirIdleState(), true); } public void Deactivate(Squad owner) { } } }