Extract BaseBuilderBotModule from HackyAI
This commit is contained in:
@@ -11,12 +11,15 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.AI
|
||||
{
|
||||
public enum BuildingType { Building, Defense, Refinery }
|
||||
|
||||
public enum WaterCheck { NotChecked, EnoughWater, NotEnoughWater }
|
||||
|
||||
public class CaptureTarget<TInfoType> where TInfoType : class, ITraitInfoInterface
|
||||
{
|
||||
internal readonly Actor Actor;
|
||||
@@ -48,6 +51,40 @@ namespace OpenRA.Mods.Common.AI
|
||||
.Any(availableCells => availableCells > 0);
|
||||
}
|
||||
|
||||
public static IEnumerable<ProductionQueue> FindQueues(Player player, string category)
|
||||
{
|
||||
return player.World.ActorsWithTrait<ProductionQueue>()
|
||||
.Where(a => a.Actor.Owner == player && a.Trait.Info.Type == category && a.Trait.Enabled)
|
||||
.Select(a => a.Trait);
|
||||
}
|
||||
|
||||
public static IEnumerable<Actor> GetActorsWithTrait<T>(World world)
|
||||
{
|
||||
return world.ActorsHavingTrait<T>();
|
||||
}
|
||||
|
||||
public static int CountActorsWithTrait<T>(string actorName, Player owner)
|
||||
{
|
||||
return GetActorsWithTrait<T>(owner.World).Count(a => a.Owner == owner && a.Info.Name == actorName);
|
||||
}
|
||||
|
||||
public static int CountBuildingByCommonName(HashSet<string> buildings, Player owner)
|
||||
{
|
||||
return GetActorsWithTrait<Building>(owner.World)
|
||||
.Count(a => a.Owner == owner && buildings.Contains(a.Info.Name));
|
||||
}
|
||||
|
||||
public static List<Actor> FindEnemiesByCommonName(HashSet<string> commonNames, Player player)
|
||||
{
|
||||
return player.World.Actors.Where(a => !a.IsDead && player.Stances[a.Owner] == Stance.Enemy &&
|
||||
commonNames.Contains(a.Info.Name)).ToList();
|
||||
}
|
||||
|
||||
public static ActorInfo GetInfoByCommonName(HashSet<string> names, Player owner)
|
||||
{
|
||||
return owner.World.Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(owner.World.LocalRandom).Value;
|
||||
}
|
||||
|
||||
public static void BotDebug(string s, params object[] args)
|
||||
{
|
||||
if (Game.Settings.Debug.BotDebug)
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
public readonly HashSet<string> ExcludeFromSquads = new HashSet<string>();
|
||||
}
|
||||
|
||||
// TODO: Move this to SquadManagerBotModule later
|
||||
public class BuildingCategories
|
||||
{
|
||||
public readonly HashSet<string> ConstructionYard = new HashSet<string>();
|
||||
@@ -53,12 +54,6 @@ namespace OpenRA.Mods.Common.AI
|
||||
[Desc("Random number of up to this many units is added to squad size when creating an attack squad.")]
|
||||
public readonly int SquadSizeRandomBonus = 30;
|
||||
|
||||
[Desc("Production queues AI uses for buildings.")]
|
||||
public readonly HashSet<string> BuildingQueues = new HashSet<string> { "Building" };
|
||||
|
||||
[Desc("Production queues AI uses for defenses.")]
|
||||
public readonly HashSet<string> DefenseQueues = new HashSet<string> { "Defense" };
|
||||
|
||||
[Desc("Delay (in ticks) between giving out orders to units.")]
|
||||
public readonly int AssignRolesInterval = 20;
|
||||
|
||||
@@ -74,51 +69,6 @@ namespace OpenRA.Mods.Common.AI
|
||||
[Desc("Minimum portion of pending orders to issue each tick (e.g. 5 issues at least 1/5th of all pending orders). Excess orders remain queued for subsequent ticks.")]
|
||||
public readonly int MinOrderQuotientPerTick = 5;
|
||||
|
||||
[Desc("Minimum excess power the AI should try to maintain.")]
|
||||
public readonly int MinimumExcessPower = 0;
|
||||
|
||||
[Desc("Increase maintained excess power by this amount for every ExcessPowerIncreaseThreshold of base buildings.")]
|
||||
public readonly int ExcessPowerIncrement = 0;
|
||||
|
||||
[Desc("Increase maintained excess power by ExcessPowerIncrement for every N base buildings.")]
|
||||
public readonly int ExcessPowerIncreaseThreshold = 1;
|
||||
|
||||
[Desc("The targeted excess power the AI tries to maintain cannot rise above this.")]
|
||||
public readonly int MaximumExcessPower = 0;
|
||||
|
||||
[Desc("Additional delay (in ticks) between structure production checks when there is no active production.",
|
||||
"StructureProductionRandomBonusDelay is added to this.")]
|
||||
public readonly int StructureProductionInactiveDelay = 125;
|
||||
|
||||
[Desc("Additional delay (in ticks) added between structure production checks when actively building things.",
|
||||
"Note: The total delay is gamespeed OrderLatency x 4 + this + StructureProductionRandomBonusDelay.")]
|
||||
public readonly int StructureProductionActiveDelay = 0;
|
||||
|
||||
[Desc("A random delay (in ticks) of up to this is added to active/inactive production delays.")]
|
||||
public readonly int StructureProductionRandomBonusDelay = 10;
|
||||
|
||||
[Desc("Delay (in ticks) until retrying to build structure after the last 3 consecutive attempts failed.")]
|
||||
public readonly int StructureProductionResumeDelay = 1500;
|
||||
|
||||
[Desc("After how many failed attempts to place a structure should AI give up and wait",
|
||||
"for StructureProductionResumeDelay before retrying.")]
|
||||
public readonly int MaximumFailedPlacementAttempts = 3;
|
||||
|
||||
[Desc("How many randomly chosen cells with resources to check when deciding refinery placement.")]
|
||||
public readonly int MaxResourceCellsToCheck = 3;
|
||||
|
||||
[Desc("Delay (in ticks) until rechecking for new BaseProviders.")]
|
||||
public readonly int CheckForNewBasesDelay = 1500;
|
||||
|
||||
[Desc("Minimum range at which to build defensive structures near a combat hotspot.")]
|
||||
public readonly int MinimumDefenseRadius = 5;
|
||||
|
||||
[Desc("Maximum range at which to build defensive structures near a combat hotspot.")]
|
||||
public readonly int MaximumDefenseRadius = 20;
|
||||
|
||||
[Desc("Try to build another production building if there is too much cash.")]
|
||||
public readonly int NewProductionCashThreshold = 5000;
|
||||
|
||||
[Desc("Only produce units as long as there are less than this amount of units idling inside the base.")]
|
||||
public readonly int IdleBaseUnitsMaximum = 12;
|
||||
|
||||
@@ -128,13 +78,11 @@ namespace OpenRA.Mods.Common.AI
|
||||
[Desc("Radius in cells around the base that should be scanned for units to be protected.")]
|
||||
public readonly int ProtectUnitScanRadius = 15;
|
||||
|
||||
[Desc("Radius in cells around a factory scanned for rally points by the AI.")]
|
||||
public readonly int RallyPointScanRadius = 8;
|
||||
|
||||
[Desc("Minimum distance in cells from center of the base when checking for building placement.")]
|
||||
[Desc("Minimum distance in cells from center of the base when checking for MCV deployment location.")]
|
||||
public readonly int MinBaseRadius = 2;
|
||||
|
||||
[Desc("Radius in cells around the center of the base to expand.")]
|
||||
[Desc("Maximum distance in cells from center of the base when checking for MCV deployment location.",
|
||||
"Only applies if RestrictMCVDeploymentFallbackToBase is enabled and there's at least one construction yard.")]
|
||||
public readonly int MaxBaseRadius = 20;
|
||||
|
||||
[Desc("Radius in cells that squads should scan for enemies around their position while idle.")]
|
||||
@@ -152,41 +100,25 @@ namespace OpenRA.Mods.Common.AI
|
||||
[Desc("Should deployment of additional MCVs be restricted to MaxBaseRadius if explicit deploy locations are missing or occupied?")]
|
||||
public readonly bool RestrictMCVDeploymentFallbackToBase = true;
|
||||
|
||||
[Desc("Radius in cells around each building with ProvideBuildableArea",
|
||||
"to check for a 3x3 area of water where naval structures can be built.",
|
||||
"Should match maximum adjacency of naval structures.")]
|
||||
public readonly int CheckForWaterRadius = 8;
|
||||
|
||||
[Desc("Terrain types which are considered water for base building purposes.")]
|
||||
public readonly HashSet<string> WaterTerrainTypes = new HashSet<string> { "Water" };
|
||||
|
||||
[Desc("Production queues AI uses for producing units.")]
|
||||
public readonly HashSet<string> UnitQueues = new HashSet<string> { "Vehicle", "Infantry", "Plane", "Ship", "Aircraft" };
|
||||
|
||||
[Desc("Should the AI repair its buildings if damaged?")]
|
||||
public readonly bool ShouldRepairBuildings = true;
|
||||
|
||||
[Desc("What units to the AI should build.", "What % of the total army must be this type of unit.")]
|
||||
public readonly Dictionary<string, float> UnitsToBuild = null;
|
||||
|
||||
[Desc("What units should the AI have a maximum limit to train.")]
|
||||
public readonly Dictionary<string, int> UnitLimits = null;
|
||||
|
||||
[Desc("What buildings to the AI should build.", "What % of the total base must be this type of building.")]
|
||||
public readonly Dictionary<string, float> BuildingFractions = null;
|
||||
|
||||
[Desc("Tells the AI what unit types fall under the same common name. Supported entries are Mcv and ExcludeFromSquads.")]
|
||||
[FieldLoader.LoadUsing("LoadUnitCategories", true)]
|
||||
public readonly UnitCategories UnitsCommonNames;
|
||||
|
||||
// TODO: Move this to SquadManagerBotModule later
|
||||
[Desc("Tells the AI what building types fall under the same common name.",
|
||||
"Possible keys are ConstructionYard, Power, Refinery, Silo, Barracks, Production, VehiclesFactory, NavalProduction.")]
|
||||
[FieldLoader.LoadUsing("LoadBuildingCategories", true)]
|
||||
public readonly BuildingCategories BuildingCommonNames;
|
||||
|
||||
[Desc("What buildings should the AI have a maximum limit to build.")]
|
||||
public readonly Dictionary<string, int> BuildingLimits = null;
|
||||
|
||||
[Desc("Actor types that can capture other actors (via `Captures` or `ExternalCaptures`).",
|
||||
"Leave this empty to disable capturing.")]
|
||||
public HashSet<string> CapturingActorTypes = new HashSet<string>();
|
||||
@@ -214,6 +146,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
return FieldLoader.Load<UnitCategories>(categories.Value);
|
||||
}
|
||||
|
||||
// TODO: Move this to SquadManagerBotModule later
|
||||
static object LoadBuildingCategories(MiniYaml yaml)
|
||||
{
|
||||
var categories = yaml.Nodes.First(n => n.Key == "BuildingCommonNames");
|
||||
@@ -227,7 +160,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
public object Create(ActorInitializer init) { return new HackyAI(this, init); }
|
||||
}
|
||||
|
||||
public sealed class HackyAI : ITick, IBot, INotifyDamage
|
||||
public sealed class HackyAI : ITick, IBot, INotifyDamage, IBotPositionsUpdated
|
||||
{
|
||||
// DEPRECATED: Modules should use World.LocalRandom.
|
||||
public MersenneTwister Random { get; private set; }
|
||||
@@ -252,16 +185,12 @@ namespace OpenRA.Mods.Common.AI
|
||||
readonly Predicate<Actor> unitCannotBeOrdered;
|
||||
|
||||
IBotTick[] tickModules;
|
||||
IBotRespondToAttack[] attackResponseModules;
|
||||
IBotPositionsUpdated[] positionsUpdatedModules;
|
||||
|
||||
CPos initialBaseCenter;
|
||||
PowerManager playerPower;
|
||||
PlayerResources playerResource;
|
||||
int ticks;
|
||||
|
||||
BitArray resourceTypeIndices;
|
||||
|
||||
List<BaseBuilder> builders = new List<BaseBuilder>();
|
||||
|
||||
List<Actor> unitsHangingAroundTheBase = new List<Actor>();
|
||||
|
||||
// Units that the ai already knows about. Any unit not on this list needs to be given a role.
|
||||
@@ -303,14 +232,9 @@ namespace OpenRA.Mods.Common.AI
|
||||
{
|
||||
Player = p;
|
||||
IsEnabled = true;
|
||||
playerPower = p.PlayerActor.TraitOrDefault<PowerManager>();
|
||||
playerResource = p.PlayerActor.Trait<PlayerResources>();
|
||||
tickModules = p.PlayerActor.TraitsImplementing<IBotTick>().ToArray();
|
||||
|
||||
foreach (var building in Info.BuildingQueues)
|
||||
builders.Add(new BaseBuilder(this, building, p, playerPower, playerResource));
|
||||
foreach (var defense in Info.DefenseQueues)
|
||||
builders.Add(new BaseBuilder(this, defense, p, playerPower, playerResource));
|
||||
attackResponseModules = p.PlayerActor.TraitsImplementing<IBotRespondToAttack>().ToArray();
|
||||
positionsUpdatedModules = p.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
|
||||
|
||||
Random = new MersenneTwister(Game.CosmeticRandom.Next());
|
||||
|
||||
@@ -323,11 +247,6 @@ namespace OpenRA.Mods.Common.AI
|
||||
attackForceTicks = Random.Next(0, Info.AttackForceInterval);
|
||||
minAttackForceDelayTicks = Random.Next(0, Info.MinimumAttackForceDelay);
|
||||
minCaptureDelayTicks = Random.Next(0, Info.MinimumCaptureDelay);
|
||||
|
||||
var tileset = World.Map.Rules.TileSet;
|
||||
resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough
|
||||
foreach (var t in Map.Rules.Actors["world"].TraitInfos<ResourceTypeInfo>())
|
||||
resourceTypeIndices.Set(tileset.GetTerrainIndex(t.TerrainType), true);
|
||||
}
|
||||
|
||||
void IBot.QueueOrder(Order order)
|
||||
@@ -371,49 +290,25 @@ namespace OpenRA.Mods.Common.AI
|
||||
return null;
|
||||
}
|
||||
|
||||
IEnumerable<Actor> GetActorsWithTrait<T>()
|
||||
bool HasAdequateConstructionYardCount
|
||||
{
|
||||
return World.ActorsHavingTrait<T>();
|
||||
get
|
||||
{
|
||||
// Require at least one construction yard, unless we have no vehicles factory (can't build it).
|
||||
return AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 ||
|
||||
AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int CountActorsWithTrait<T>(string actorName, Player owner)
|
||||
bool HasAdequateRefineryCount
|
||||
{
|
||||
return GetActorsWithTrait<T>().Count(a => a.Owner == owner && a.Info.Name == actorName);
|
||||
}
|
||||
|
||||
int CountBuildingByCommonName(HashSet<string> buildings, Player owner)
|
||||
{
|
||||
return GetActorsWithTrait<Building>()
|
||||
.Count(a => a.Owner == owner && buildings.Contains(a.Info.Name));
|
||||
}
|
||||
|
||||
public ActorInfo GetInfoByCommonName(HashSet<string> names, Player owner)
|
||||
{
|
||||
return Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(Random).Value;
|
||||
}
|
||||
|
||||
public bool HasAdequateFact()
|
||||
{
|
||||
// Require at least one construction yard, unless we have no vehicles factory (can't build it).
|
||||
return CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0 ||
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.VehiclesFactory, Player) == 0;
|
||||
}
|
||||
|
||||
public bool HasAdequateProc()
|
||||
{
|
||||
// Require at least one refinery, unless we can't build it.
|
||||
return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 ||
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 ||
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) == 0;
|
||||
}
|
||||
|
||||
public bool HasMinimumProc()
|
||||
{
|
||||
// Require at least two refineries, unless we have no power (can't build it)
|
||||
// or barracks (higher priority?)
|
||||
return CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) >= 2 ||
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 ||
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.Barracks, Player) == 0;
|
||||
get
|
||||
{
|
||||
// Require at least one refinery, unless we can't build it.
|
||||
return AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.Refinery, Player) > 0 ||
|
||||
AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.Power, Player) == 0 ||
|
||||
AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
// For mods like RA (number of RearmActors must match the number of aircraft)
|
||||
@@ -428,39 +323,38 @@ namespace OpenRA.Mods.Common.AI
|
||||
if (rearmableInfo == null)
|
||||
return true;
|
||||
|
||||
var countOwnAir = CountActorsWithTrait<IPositionable>(actorInfo.Name, Player);
|
||||
var countBuildings = rearmableInfo.RearmActors.Sum(b => CountActorsWithTrait<Building>(b, Player));
|
||||
var countOwnAir = AIUtils.CountActorsWithTrait<IPositionable>(actorInfo.Name, Player);
|
||||
var countBuildings = rearmableInfo.RearmActors.Sum(b => AIUtils.CountActorsWithTrait<Building>(b, Player));
|
||||
if (countOwnAir >= countBuildings)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
CPos defenseCenter;
|
||||
public CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type)
|
||||
CPos? ChooseMcvDeployLocation(string actorType, bool distanceToBaseIsImportant)
|
||||
{
|
||||
var ai = Map.Rules.Actors[actorType];
|
||||
var bi = ai.TraitInfoOrDefault<BuildingInfo>();
|
||||
var actorInfo = World.Map.Rules.Actors[actorType];
|
||||
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
|
||||
if (bi == null)
|
||||
return null;
|
||||
|
||||
// Find the buildable cell that is closest to pos and centered around center
|
||||
Func<CPos, CPos, int, int, CPos?> findPos = (center, target, minRange, maxRange) =>
|
||||
{
|
||||
var cells = Map.FindTilesInAnnulus(center, minRange, maxRange);
|
||||
var cells = World.Map.FindTilesInAnnulus(center, minRange, maxRange);
|
||||
|
||||
// Sort by distance to target if we have one
|
||||
if (center != target)
|
||||
cells = cells.OrderBy(c => (c - target).LengthSquared);
|
||||
else
|
||||
cells = cells.Shuffle(Random);
|
||||
cells = cells.Shuffle(World.LocalRandom);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (!World.CanPlaceBuilding(cell, ai, bi, null))
|
||||
if (!World.CanPlaceBuilding(cell, actorInfo, bi, null))
|
||||
continue;
|
||||
|
||||
if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(World, Player, ai, cell))
|
||||
if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(World, Player, actorInfo, cell))
|
||||
continue;
|
||||
|
||||
return cell;
|
||||
@@ -471,40 +365,8 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
var baseCenter = GetRandomBaseCenter();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case BuildingType.Defense:
|
||||
|
||||
// Build near the closest enemy structure
|
||||
var closestEnemy = World.ActorsHavingTrait<Building>().Where(a => !a.Disposed && Player.Stances[a.Owner] == Stance.Enemy)
|
||||
.ClosestTo(World.Map.CenterOfCell(defenseCenter));
|
||||
|
||||
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
|
||||
return findPos(defenseCenter, targetCell, Info.MinimumDefenseRadius, Info.MaximumDefenseRadius);
|
||||
|
||||
case BuildingType.Refinery:
|
||||
|
||||
// Try and place the refinery near a resource field
|
||||
var nearbyResources = Map.FindTilesInAnnulus(baseCenter, Info.MinBaseRadius, Info.MaxBaseRadius)
|
||||
.Where(a => resourceTypeIndices.Get(Map.GetTerrainIndex(a)))
|
||||
.Shuffle(Random).Take(Info.MaxResourceCellsToCheck);
|
||||
|
||||
foreach (var r in nearbyResources)
|
||||
{
|
||||
var found = findPos(baseCenter, r, Info.MinBaseRadius, Info.MaxBaseRadius);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
// Try and find a free spot somewhere else in the base
|
||||
return findPos(baseCenter, baseCenter, Info.MinBaseRadius, Info.MaxBaseRadius);
|
||||
|
||||
case BuildingType.Building:
|
||||
return findPos(baseCenter, baseCenter, Info.MinBaseRadius, distanceToBaseIsImportant ? Info.MaxBaseRadius : Map.Grid.MaximumTileSearchRange);
|
||||
}
|
||||
|
||||
// Can't find a build location
|
||||
return null;
|
||||
return findPos(baseCenter, baseCenter, Info.MinBaseRadius,
|
||||
distanceToBaseIsImportant ? Info.MaxBaseRadius : World.Map.Grid.MaximumTileSearchRange);
|
||||
}
|
||||
|
||||
void ITick.Tick(Actor self)
|
||||
@@ -521,15 +383,11 @@ namespace OpenRA.Mods.Common.AI
|
||||
ProductionUnits(self);
|
||||
|
||||
AssignRolesToIdleUnits(self);
|
||||
SetRallyPointsForNewProductionBuildings(self);
|
||||
|
||||
foreach (var b in builders)
|
||||
b.Tick();
|
||||
|
||||
// TODO: Add an option to include this in CheckSyncAroundUnsyncedCode.
|
||||
// TODO: Add an option to include this in CheckSyncUnchanged.
|
||||
// Checking sync for this is too expensive to include it by default,
|
||||
// so it should be implemented as separate sub-option checkbox.
|
||||
using (new PerfSample("tick_bots"))
|
||||
using (new PerfSample("bot_tick"))
|
||||
foreach (var t in tickModules)
|
||||
if (t.IsTraitEnabled())
|
||||
t.BotTick(this);
|
||||
@@ -549,12 +407,6 @@ namespace OpenRA.Mods.Common.AI
|
||||
return World.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestTo(pos);
|
||||
}
|
||||
|
||||
List<Actor> FindEnemyConstructionYards()
|
||||
{
|
||||
return World.Actors.Where(a => Player.Stances[a.Owner] == Stance.Enemy && !a.IsDead &&
|
||||
Info.BuildingCommonNames.ConstructionYard.Contains(a.Info.Name)).ToList();
|
||||
}
|
||||
|
||||
void CleanSquads()
|
||||
{
|
||||
Squads.RemoveAll(s => !s.IsValid);
|
||||
@@ -752,7 +604,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
void TryToRushAttack()
|
||||
{
|
||||
var allEnemyBaseBuilder = FindEnemyConstructionYards();
|
||||
var allEnemyBaseBuilder = AIUtils.FindEnemiesByCommonName(Info.BuildingCommonNames.ConstructionYard, Player);
|
||||
var ownUnits = activeUnits
|
||||
.Where(unit => unit.IsIdle && unit.Info.HasTraitInfo<AttackBaseInfo>()
|
||||
&& !unit.Info.HasTraitInfo<AircraftInfo>() && !unit.Info.HasTraitInfo<HarvesterInfo>()).ToList();
|
||||
@@ -800,41 +652,6 @@ namespace OpenRA.Mods.Common.AI
|
||||
}
|
||||
}
|
||||
|
||||
bool IsRallyPointValid(CPos x, BuildingInfo info)
|
||||
{
|
||||
return info != null && World.IsCellBuildable(x, null, info);
|
||||
}
|
||||
|
||||
void SetRallyPointsForNewProductionBuildings(Actor self)
|
||||
{
|
||||
foreach (var rp in self.World.ActorsWithTrait<RallyPoint>())
|
||||
{
|
||||
if (rp.Actor.Owner == Player &&
|
||||
!IsRallyPointValid(rp.Trait.Location, rp.Actor.Info.TraitInfoOrDefault<BuildingInfo>()))
|
||||
{
|
||||
QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(World, ChooseRallyLocationNear(rp.Actor)), false)
|
||||
{
|
||||
SuppressVisualFeedback = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Won't work for shipyards...
|
||||
CPos ChooseRallyLocationNear(Actor producer)
|
||||
{
|
||||
var possibleRallyPoints = Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius)
|
||||
.Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault<BuildingInfo>()));
|
||||
|
||||
if (!possibleRallyPoints.Any())
|
||||
{
|
||||
AIUtils.BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location);
|
||||
return producer.Location;
|
||||
}
|
||||
|
||||
return possibleRallyPoints.Random(Random);
|
||||
}
|
||||
|
||||
void InitializeBase(Actor self, bool chooseLocation)
|
||||
{
|
||||
var mcv = FindAndDeployMcv(self, chooseLocation);
|
||||
@@ -842,10 +659,20 @@ namespace OpenRA.Mods.Common.AI
|
||||
if (mcv == null)
|
||||
return;
|
||||
|
||||
initialBaseCenter = mcv.Location;
|
||||
defenseCenter = mcv.Location;
|
||||
foreach (var n in positionsUpdatedModules)
|
||||
{
|
||||
n.UpdatedBaseCenter(mcv.Location);
|
||||
n.UpdatedDefenseCenter(mcv.Location);
|
||||
}
|
||||
}
|
||||
|
||||
void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)
|
||||
{
|
||||
initialBaseCenter = newLocation;
|
||||
}
|
||||
|
||||
void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { }
|
||||
|
||||
// Find any MCV and deploy them at a sensible location.
|
||||
Actor FindAndDeployMcv(Actor self, bool move)
|
||||
{
|
||||
@@ -862,11 +689,11 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
// If we lack a base, we need to make sure we don't restrict deployment of the MCV to the base!
|
||||
var restrictToBase = Info.RestrictMCVDeploymentFallbackToBase &&
|
||||
CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0;
|
||||
AIUtils.CountBuildingByCommonName(Info.BuildingCommonNames.ConstructionYard, Player) > 0;
|
||||
|
||||
if (move)
|
||||
{
|
||||
var desiredLocation = ChooseBuildLocation(transformsInfo.IntoActor, restrictToBase, BuildingType.Building);
|
||||
var desiredLocation = ChooseMcvDeployLocation(transformsInfo.IntoActor, restrictToBase);
|
||||
if (desiredLocation == null)
|
||||
return null;
|
||||
|
||||
@@ -878,23 +705,16 @@ namespace OpenRA.Mods.Common.AI
|
||||
return mcv;
|
||||
}
|
||||
|
||||
internal IEnumerable<ProductionQueue> FindQueues(string category)
|
||||
{
|
||||
return World.ActorsWithTrait<ProductionQueue>()
|
||||
.Where(a => a.Actor.Owner == Player && a.Trait.Info.Type == category && a.Trait.Enabled)
|
||||
.Select(a => a.Trait);
|
||||
}
|
||||
|
||||
void ProductionUnits(Actor self)
|
||||
{
|
||||
// Stop building until economy is restored
|
||||
if (!HasAdequateProc())
|
||||
if (!HasAdequateRefineryCount)
|
||||
return;
|
||||
|
||||
// No construction yards - Build a new MCV
|
||||
if (Info.UnitsCommonNames.Mcv.Any() && !HasAdequateFact() && !self.World.Actors.Any(a => a.Owner == Player &&
|
||||
Info.UnitsCommonNames.Mcv.Contains(a.Info.Name)))
|
||||
BuildUnit("Vehicle", GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name);
|
||||
if (Info.UnitsCommonNames.Mcv.Any() && HasAdequateConstructionYardCount &&
|
||||
!self.World.Actors.Any(a => a.Owner == Player && Info.UnitsCommonNames.Mcv.Contains(a.Info.Name)))
|
||||
BuildUnit("Vehicle", AIUtils.GetInfoByCommonName(Info.UnitsCommonNames.Mcv, Player).Name);
|
||||
|
||||
foreach (var q in Info.UnitQueues)
|
||||
BuildUnit(q, unitsHangingAroundTheBase.Count < Info.IdleBaseUnitsMaximum);
|
||||
@@ -903,7 +723,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
void BuildUnit(string category, bool buildRandom)
|
||||
{
|
||||
// Pick a free queue
|
||||
var queue = FindQueues(category).FirstOrDefault(q => !q.AllQueued().Any());
|
||||
var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any());
|
||||
if (queue == null)
|
||||
return;
|
||||
|
||||
@@ -929,7 +749,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
void BuildUnit(string category, string name)
|
||||
{
|
||||
var queue = FindQueues(category).FirstOrDefault(q => !q.AllQueued().Any());
|
||||
var queue = AIUtils.FindQueues(Player, category).FirstOrDefault(q => !q.AllQueued().Any());
|
||||
if (queue == null)
|
||||
return;
|
||||
|
||||
@@ -939,37 +759,30 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
void INotifyDamage.Damaged(Actor self, AttackInfo e)
|
||||
{
|
||||
if (!IsEnabled || e.Attacker == null)
|
||||
if (!IsEnabled)
|
||||
return;
|
||||
|
||||
if (e.Attacker.Owner.Stances[self.Owner] == Stance.Neutral)
|
||||
// TODO: Add an option to include this in CheckSyncUnchanged.
|
||||
// Checking sync for this is too expensive to include it by default,
|
||||
// so it should be implemented as separate sub-option checkbox.
|
||||
using (new PerfSample("bot_attack_response"))
|
||||
foreach (var t in attackResponseModules)
|
||||
if (t.IsTraitEnabled())
|
||||
t.RespondToAttack(this, self, e);
|
||||
|
||||
if (e.Attacker == null || e.Attacker.Disposed)
|
||||
return;
|
||||
|
||||
var rb = self.TraitOrDefault<RepairableBuilding>();
|
||||
|
||||
if (Info.ShouldRepairBuildings && rb != null)
|
||||
{
|
||||
if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light && !rb.RepairActive)
|
||||
{
|
||||
AIUtils.BotDebug("Bot noticed damage {0} {1}->{2}, repairing.",
|
||||
self, e.PreviousDamageState, e.DamageState);
|
||||
QueueOrder(new Order("RepairBuilding", self.Owner.PlayerActor, Target.FromActor(self), false));
|
||||
}
|
||||
}
|
||||
|
||||
if (e.Attacker.Disposed)
|
||||
if (e.Attacker.Owner.Stances[self.Owner] != Stance.Enemy)
|
||||
return;
|
||||
|
||||
if (!e.Attacker.Info.HasTraitInfo<ITargetableInfo>())
|
||||
return;
|
||||
|
||||
// Protected priority assets, MCVs, harvesters and buildings
|
||||
if ((self.Info.HasTraitInfo<HarvesterInfo>() || self.Info.HasTraitInfo<BuildingInfo>() || self.Info.HasTraitInfo<BaseBuildingInfo>()) &&
|
||||
Player.Stances[e.Attacker.Owner] == Stance.Enemy)
|
||||
{
|
||||
defenseCenter = e.Attacker.Location;
|
||||
// TODO: Use *CommonNames, instead of hard-coding trait(info)s.
|
||||
if (self.Info.HasTraitInfo<HarvesterInfo>() || self.Info.HasTraitInfo<BuildingInfo>() || self.Info.HasTraitInfo<BaseBuildingInfo>())
|
||||
ProtectOwn(e.Attacker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +122,12 @@
|
||||
<Compile Include="ActorExts.cs" />
|
||||
<Compile Include="AI\AIUtils.cs" />
|
||||
<Compile Include="AI\AttackOrFleeFuzzy.cs" />
|
||||
<Compile Include="AI\BaseBuilder.cs" />
|
||||
<Compile Include="Traits\BotModules\BotModuleLogic\BaseBuilderQueueManager.cs" />
|
||||
<Compile Include="AI\HackyAI.cs" />
|
||||
<Compile Include="Traits\BotModules\HarvesterBotModule.cs" />
|
||||
<Compile Include="Traits\BotModules\SupportPowerBotModule.cs" />
|
||||
<Compile Include="Traits\BotModules\BaseBuilderBotModule.cs" />
|
||||
<Compile Include="Traits\BotModules\BuildingRepairBotModule.cs" />
|
||||
<Compile Include="AI\Squad.cs" />
|
||||
<Compile Include="AI\StateMachine.cs" />
|
||||
<Compile Include="AI\States\AirStates.cs" />
|
||||
|
||||
244
OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs
Normal file
244
OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2018 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;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.AI;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[Desc("Manages AI base construction.")]
|
||||
public class BaseBuilderBotModuleInfo : ConditionalTraitInfo
|
||||
{
|
||||
[Desc("Tells the AI what building types are considered construction yards.")]
|
||||
public readonly HashSet<string> ConstructionYardTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered vehicle production facilities.")]
|
||||
public readonly HashSet<string> VehiclesFactoryTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered refineries.")]
|
||||
public readonly HashSet<string> RefineryTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered power plants.")]
|
||||
public readonly HashSet<string> PowerTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered infantry production facilities.")]
|
||||
public readonly HashSet<string> BarracksTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered production facilities.")]
|
||||
public readonly HashSet<string> ProductionTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered naval production facilities.")]
|
||||
public readonly HashSet<string> NavalProductionTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Tells the AI what building types are considered silos (resource storage).")]
|
||||
public readonly HashSet<string> SiloTypes = new HashSet<string>();
|
||||
|
||||
[Desc("Production queues AI uses for buildings.")]
|
||||
public readonly HashSet<string> BuildingQueues = new HashSet<string> { "Building" };
|
||||
|
||||
[Desc("Production queues AI uses for defenses.")]
|
||||
public readonly HashSet<string> DefenseQueues = new HashSet<string> { "Defense" };
|
||||
|
||||
[Desc("Minimum distance in cells from center of the base when checking for building placement.")]
|
||||
public readonly int MinBaseRadius = 2;
|
||||
|
||||
[Desc("Radius in cells around the center of the base to expand.")]
|
||||
public readonly int MaxBaseRadius = 20;
|
||||
|
||||
[Desc("Minimum excess power the AI should try to maintain.")]
|
||||
public readonly int MinimumExcessPower = 0;
|
||||
|
||||
[Desc("The targeted excess power the AI tries to maintain cannot rise above this.")]
|
||||
public readonly int MaximumExcessPower = 0;
|
||||
|
||||
[Desc("Increase maintained excess power by this amount for every ExcessPowerIncreaseThreshold of base buildings.")]
|
||||
public readonly int ExcessPowerIncrement = 0;
|
||||
|
||||
[Desc("Increase maintained excess power by ExcessPowerIncrement for every N base buildings.")]
|
||||
public readonly int ExcessPowerIncreaseThreshold = 1;
|
||||
|
||||
[Desc("Additional delay (in ticks) between structure production checks when there is no active production.",
|
||||
"StructureProductionRandomBonusDelay is added to this.")]
|
||||
public readonly int StructureProductionInactiveDelay = 125;
|
||||
|
||||
[Desc("Additional delay (in ticks) added between structure production checks when actively building things.",
|
||||
"Note: The total delay is gamespeed OrderLatency x 4 + this + StructureProductionRandomBonusDelay.")]
|
||||
public readonly int StructureProductionActiveDelay = 0;
|
||||
|
||||
[Desc("A random delay (in ticks) of up to this is added to active/inactive production delays.")]
|
||||
public readonly int StructureProductionRandomBonusDelay = 10;
|
||||
|
||||
[Desc("Delay (in ticks) until retrying to build structure after the last 3 consecutive attempts failed.")]
|
||||
public readonly int StructureProductionResumeDelay = 1500;
|
||||
|
||||
[Desc("After how many failed attempts to place a structure should AI give up and wait",
|
||||
"for StructureProductionResumeDelay before retrying.")]
|
||||
public readonly int MaximumFailedPlacementAttempts = 3;
|
||||
|
||||
[Desc("How many randomly chosen cells with resources to check when deciding refinery placement.")]
|
||||
public readonly int MaxResourceCellsToCheck = 3;
|
||||
|
||||
[Desc("Delay (in ticks) until rechecking for new BaseProviders.")]
|
||||
public readonly int CheckForNewBasesDelay = 1500;
|
||||
|
||||
[Desc("Minimum range at which to build defensive structures near a combat hotspot.")]
|
||||
public readonly int MinimumDefenseRadius = 5;
|
||||
|
||||
[Desc("Maximum range at which to build defensive structures near a combat hotspot.")]
|
||||
public readonly int MaximumDefenseRadius = 20;
|
||||
|
||||
[Desc("Try to build another production building if there is too much cash.")]
|
||||
public readonly int NewProductionCashThreshold = 5000;
|
||||
|
||||
[Desc("Radius in cells around a factory scanned for rally points by the AI.")]
|
||||
public readonly int RallyPointScanRadius = 8;
|
||||
|
||||
[Desc("Radius in cells around each building with ProvideBuildableArea",
|
||||
"to check for a 3x3 area of water where naval structures can be built.",
|
||||
"Should match maximum adjacency of naval structures.")]
|
||||
public readonly int CheckForWaterRadius = 8;
|
||||
|
||||
[Desc("Terrain types which are considered water for base building purposes.")]
|
||||
public readonly HashSet<string> WaterTerrainTypes = new HashSet<string> { "Water" };
|
||||
|
||||
[Desc("What buildings to the AI should build.", "What integer percentage of the total base must be this type of building.")]
|
||||
public readonly Dictionary<string, int> BuildingFractions = null;
|
||||
|
||||
[Desc("What buildings should the AI have a maximum limit to build.")]
|
||||
public readonly Dictionary<string, int> BuildingLimits = null;
|
||||
|
||||
public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); }
|
||||
}
|
||||
|
||||
public class BaseBuilderBotModule : ConditionalTrait<BaseBuilderBotModuleInfo>, IBotTick, IBotPositionsUpdated, IBotRespondToAttack
|
||||
{
|
||||
public CPos GetRandomBaseCenter()
|
||||
{
|
||||
var randomConstructionYard = world.Actors.Where(a => a.Owner == player &&
|
||||
Info.ConstructionYardTypes.Contains(a.Info.Name))
|
||||
.RandomOrDefault(world.LocalRandom);
|
||||
|
||||
return randomConstructionYard != null ? randomConstructionYard.Location : initialBaseCenter;
|
||||
}
|
||||
|
||||
public CPos DefenseCenter { get { return defenseCenter; } }
|
||||
|
||||
readonly World world;
|
||||
readonly Player player;
|
||||
PowerManager playerPower;
|
||||
PlayerResources playerResources;
|
||||
IBotPositionsUpdated[] positionsUpdatedModules;
|
||||
BitArray resourceTypeIndices;
|
||||
CPos initialBaseCenter;
|
||||
CPos defenseCenter;
|
||||
|
||||
List<BaseBuilderQueueManager> builders = new List<BaseBuilderQueueManager>();
|
||||
|
||||
public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info)
|
||||
: base(info)
|
||||
{
|
||||
world = self.World;
|
||||
player = self.Owner;
|
||||
}
|
||||
|
||||
protected override void TraitEnabled(Actor self)
|
||||
{
|
||||
playerPower = player.PlayerActor.TraitOrDefault<PowerManager>();
|
||||
playerResources = player.PlayerActor.Trait<PlayerResources>();
|
||||
positionsUpdatedModules = player.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
|
||||
|
||||
var tileset = world.Map.Rules.TileSet;
|
||||
resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough
|
||||
foreach (var t in world.Map.Rules.Actors["world"].TraitInfos<ResourceTypeInfo>())
|
||||
resourceTypeIndices.Set(tileset.GetTerrainIndex(t.TerrainType), true);
|
||||
|
||||
foreach (var building in Info.BuildingQueues)
|
||||
builders.Add(new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceTypeIndices));
|
||||
foreach (var defense in Info.DefenseQueues)
|
||||
builders.Add(new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceTypeIndices));
|
||||
}
|
||||
|
||||
void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)
|
||||
{
|
||||
initialBaseCenter = newLocation;
|
||||
}
|
||||
|
||||
void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation)
|
||||
{
|
||||
defenseCenter = newLocation;
|
||||
}
|
||||
|
||||
void IBotTick.BotTick(IBot bot)
|
||||
{
|
||||
SetRallyPointsForNewProductionBuildings(bot);
|
||||
|
||||
foreach (var b in builders)
|
||||
b.Tick(bot);
|
||||
}
|
||||
|
||||
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
||||
{
|
||||
if (e.Attacker == null || e.Attacker.Disposed)
|
||||
return;
|
||||
|
||||
if (e.Attacker.Owner.Stances[self.Owner] != Stance.Enemy)
|
||||
return;
|
||||
|
||||
if (!e.Attacker.Info.HasTraitInfo<ITargetableInfo>())
|
||||
return;
|
||||
|
||||
// Protected priority assets, MCVs, harvesters and buildings
|
||||
if (self.Info.HasTraitInfo<BuildingInfo>() || self.Info.HasTraitInfo<BaseBuildingInfo>())
|
||||
foreach (var n in positionsUpdatedModules)
|
||||
n.UpdatedDefenseCenter(e.Attacker.Location);
|
||||
}
|
||||
|
||||
void SetRallyPointsForNewProductionBuildings(IBot bot)
|
||||
{
|
||||
foreach (var rp in world.ActorsWithTrait<RallyPoint>())
|
||||
{
|
||||
if (rp.Actor.Owner == player &&
|
||||
!IsRallyPointValid(rp.Trait.Location, rp.Actor.Info.TraitInfoOrDefault<BuildingInfo>()))
|
||||
{
|
||||
bot.QueueOrder(new Order("SetRallyPoint", rp.Actor, Target.FromCell(world, ChooseRallyLocationNear(rp.Actor)), false)
|
||||
{
|
||||
SuppressVisualFeedback = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Won't work for shipyards...
|
||||
CPos ChooseRallyLocationNear(Actor producer)
|
||||
{
|
||||
var possibleRallyPoints = world.Map.FindTilesInCircle(producer.Location, Info.RallyPointScanRadius)
|
||||
.Where(c => IsRallyPointValid(c, producer.Info.TraitInfoOrDefault<BuildingInfo>()));
|
||||
|
||||
if (!possibleRallyPoints.Any())
|
||||
{
|
||||
AIUtils.BotDebug("Bot Bug: No possible rallypoint near {0}", producer.Location);
|
||||
return producer.Location;
|
||||
}
|
||||
|
||||
return possibleRallyPoints.Random(world.LocalRandom);
|
||||
}
|
||||
|
||||
bool IsRallyPointValid(CPos x, BuildingInfo info)
|
||||
{
|
||||
return info != null && world.IsCellBuildable(x, null, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,19 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Mods.Common.AI;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.AI
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
class BaseBuilder
|
||||
class BaseBuilderQueueManager
|
||||
{
|
||||
readonly string category;
|
||||
|
||||
readonly HackyAI ai;
|
||||
readonly BaseBuilderBotModule baseBuilder;
|
||||
readonly World world;
|
||||
readonly Player player;
|
||||
readonly PowerManager playerPower;
|
||||
@@ -35,32 +36,28 @@ namespace OpenRA.Mods.Common.AI
|
||||
int cachedBases;
|
||||
int cachedBuildings;
|
||||
int minimumExcessPower;
|
||||
BitArray resourceTypeIndices;
|
||||
|
||||
enum Water
|
||||
WaterCheck waterState = WaterCheck.NotChecked;
|
||||
|
||||
public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm,
|
||||
PlayerResources pr, BitArray resourceTypeIndices)
|
||||
{
|
||||
NotChecked,
|
||||
EnoughWater,
|
||||
NotEnoughWater
|
||||
}
|
||||
|
||||
Water waterState = Water.NotChecked;
|
||||
|
||||
public BaseBuilder(HackyAI ai, string category, Player p, PowerManager pm, PlayerResources pr)
|
||||
{
|
||||
this.ai = ai;
|
||||
this.baseBuilder = baseBuilder;
|
||||
world = p.World;
|
||||
player = p;
|
||||
playerPower = pm;
|
||||
playerResources = pr;
|
||||
this.category = category;
|
||||
failRetryTicks = ai.Info.StructureProductionResumeDelay;
|
||||
minimumExcessPower = ai.Info.MinimumExcessPower;
|
||||
failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay;
|
||||
minimumExcessPower = baseBuilder.Info.MinimumExcessPower;
|
||||
this.resourceTypeIndices = resourceTypeIndices;
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
public void Tick(IBot bot)
|
||||
{
|
||||
// If failed to place something N consecutive times, wait M ticks until resuming building production
|
||||
if (failCount >= ai.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0)
|
||||
if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0)
|
||||
{
|
||||
var currentBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
|
||||
var baseProviders = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
||||
@@ -71,28 +68,28 @@ namespace OpenRA.Mods.Common.AI
|
||||
if (currentBuildings < cachedBuildings || baseProviders > cachedBases)
|
||||
failCount = 0;
|
||||
else
|
||||
failRetryTicks = ai.Info.StructureProductionResumeDelay;
|
||||
failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay;
|
||||
}
|
||||
|
||||
if (waterState == Water.NotChecked)
|
||||
if (waterState == WaterCheck.NotChecked)
|
||||
{
|
||||
if (AIUtils.IsAreaAvailable<BaseProvider>(ai.World, ai.Player, ai.Map, ai.Info.MaxBaseRadius, ai.Info.WaterTerrainTypes))
|
||||
waterState = Water.EnoughWater;
|
||||
if (AIUtils.IsAreaAvailable<BaseProvider>(world, player, world.Map, baseBuilder.Info.MaxBaseRadius, baseBuilder.Info.WaterTerrainTypes))
|
||||
waterState = WaterCheck.EnoughWater;
|
||||
else
|
||||
{
|
||||
waterState = Water.NotEnoughWater;
|
||||
checkForBasesTicks = ai.Info.CheckForNewBasesDelay;
|
||||
waterState = WaterCheck.NotEnoughWater;
|
||||
checkForBasesTicks = baseBuilder.Info.CheckForNewBasesDelay;
|
||||
}
|
||||
}
|
||||
|
||||
if (waterState == Water.NotEnoughWater && --checkForBasesTicks <= 0)
|
||||
if (waterState == WaterCheck.NotEnoughWater && --checkForBasesTicks <= 0)
|
||||
{
|
||||
var currentBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
||||
|
||||
if (currentBases > cachedBases)
|
||||
{
|
||||
cachedBases = currentBases;
|
||||
waterState = Water.NotChecked;
|
||||
waterState = WaterCheck.NotChecked;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,35 +98,35 @@ namespace OpenRA.Mods.Common.AI
|
||||
return;
|
||||
|
||||
playerBuildings = world.ActorsHavingTrait<Building>().Where(a => a.Owner == player).ToArray();
|
||||
var excessPowerBonus = ai.Info.ExcessPowerIncrement * (playerBuildings.Count() / ai.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue));
|
||||
minimumExcessPower = (ai.Info.MinimumExcessPower + excessPowerBonus).Clamp(ai.Info.MinimumExcessPower, ai.Info.MaximumExcessPower);
|
||||
var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Count() / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue));
|
||||
minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower);
|
||||
|
||||
var active = false;
|
||||
foreach (var queue in ai.FindQueues(category))
|
||||
if (TickQueue(queue))
|
||||
foreach (var queue in AIUtils.FindQueues(player, category))
|
||||
if (TickQueue(bot, queue))
|
||||
active = true;
|
||||
|
||||
// Add a random factor so not every AI produces at the same tick early in the game.
|
||||
// Minimum should not be negative as delays in HackyAI could be zero.
|
||||
var randomFactor = ai.Random.Next(0, ai.Info.StructureProductionRandomBonusDelay);
|
||||
var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay);
|
||||
|
||||
// Needs to be at least 4 * OrderLatency because otherwise the AI frequently duplicates build orders (i.e. makes the same build decision twice)
|
||||
waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + ai.Info.StructureProductionActiveDelay + randomFactor
|
||||
: ai.Info.StructureProductionInactiveDelay + randomFactor;
|
||||
waitTicks = active ? 4 * world.LobbyInfo.GlobalSettings.OrderLatency + baseBuilder.Info.StructureProductionActiveDelay + randomFactor
|
||||
: baseBuilder.Info.StructureProductionInactiveDelay + randomFactor;
|
||||
}
|
||||
|
||||
bool TickQueue(ProductionQueue queue)
|
||||
bool TickQueue(IBot bot, ProductionQueue queue)
|
||||
{
|
||||
var currentBuilding = queue.AllQueued().FirstOrDefault();
|
||||
|
||||
// Waiting to build something
|
||||
if (currentBuilding == null && failCount < ai.Info.MaximumFailedPlacementAttempts)
|
||||
if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts)
|
||||
{
|
||||
var item = ChooseBuildingToBuild(queue);
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
ai.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
|
||||
bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
|
||||
}
|
||||
else if (currentBuilding != null && currentBuilding.Done)
|
||||
{
|
||||
@@ -140,18 +137,18 @@ namespace OpenRA.Mods.Common.AI
|
||||
var type = BuildingType.Building;
|
||||
if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo<AttackBaseInfo>())
|
||||
type = BuildingType.Defense;
|
||||
else if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo<RefineryInfo>())
|
||||
else if (baseBuilder.Info.RefineryTypes.Contains(world.Map.Rules.Actors[currentBuilding.Item].Name))
|
||||
type = BuildingType.Refinery;
|
||||
|
||||
var location = ai.ChooseBuildLocation(currentBuilding.Item, true, type);
|
||||
var location = ChooseBuildLocation(currentBuilding.Item, true, type);
|
||||
if (location == null)
|
||||
{
|
||||
AIUtils.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item));
|
||||
ai.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1));
|
||||
bot.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1));
|
||||
failCount += failCount;
|
||||
|
||||
// If we just reached the maximum fail count, cache the number of current structures
|
||||
if (failCount == ai.Info.MaximumFailedPlacementAttempts)
|
||||
if (failCount == baseBuilder.Info.MaximumFailedPlacementAttempts)
|
||||
{
|
||||
cachedBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
|
||||
cachedBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
||||
@@ -160,7 +157,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
else
|
||||
{
|
||||
failCount = 0;
|
||||
ai.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false)
|
||||
bot.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false)
|
||||
{
|
||||
// Building to place
|
||||
TargetString = currentBuilding.Item,
|
||||
@@ -185,22 +182,22 @@ namespace OpenRA.Mods.Common.AI
|
||||
if (!actors.Contains(actor.Name))
|
||||
return false;
|
||||
|
||||
if (!ai.Info.BuildingLimits.ContainsKey(actor.Name))
|
||||
if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name))
|
||||
return true;
|
||||
|
||||
return playerBuildings.Count(a => a.Info.Name == actor.Name) < ai.Info.BuildingLimits[actor.Name];
|
||||
return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name];
|
||||
});
|
||||
|
||||
if (orderBy != null)
|
||||
return available.MaxByOrDefault(orderBy);
|
||||
|
||||
return available.RandomOrDefault(ai.Random);
|
||||
return available.RandomOrDefault(world.LocalRandom);
|
||||
}
|
||||
|
||||
bool HasSufficientPowerForActor(ActorInfo actorInfo)
|
||||
{
|
||||
return playerPower == null || (actorInfo.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault)
|
||||
.Sum(p => p.Amount) + playerPower.ExcessPower) >= minimumExcessPower;
|
||||
.Sum(p => p.Amount) + playerPower.ExcessPower) >= baseBuilder.Info.MinimumExcessPower;
|
||||
}
|
||||
|
||||
ActorInfo ChooseBuildingToBuild(ProductionQueue queue)
|
||||
@@ -208,7 +205,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
var buildableThings = queue.BuildableItems();
|
||||
|
||||
// This gets used quite a bit, so let's cache it here
|
||||
var power = GetProducibleBuilding(ai.Info.BuildingCommonNames.Power, buildableThings,
|
||||
var power = GetProducibleBuilding(baseBuilder.Info.PowerTypes, buildableThings,
|
||||
a => a.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount));
|
||||
|
||||
// First priority is to get out of a low power situation
|
||||
@@ -222,9 +219,9 @@ namespace OpenRA.Mods.Common.AI
|
||||
}
|
||||
|
||||
// Next is to build up a strong economy
|
||||
if (!ai.HasAdequateProc() || !ai.HasMinimumProc())
|
||||
if (!HasAdequateRefineryCount)
|
||||
{
|
||||
var refinery = GetProducibleBuilding(ai.Info.BuildingCommonNames.Refinery, buildableThings);
|
||||
var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings);
|
||||
if (refinery != null && HasSufficientPowerForActor(refinery))
|
||||
{
|
||||
AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name);
|
||||
@@ -239,9 +236,9 @@ namespace OpenRA.Mods.Common.AI
|
||||
}
|
||||
|
||||
// Make sure that we can spend as fast as we are earning
|
||||
if (ai.Info.NewProductionCashThreshold > 0 && playerResources.Resources > ai.Info.NewProductionCashThreshold)
|
||||
if (baseBuilder.Info.NewProductionCashThreshold > 0 && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold)
|
||||
{
|
||||
var production = GetProducibleBuilding(ai.Info.BuildingCommonNames.Production, buildableThings);
|
||||
var production = GetProducibleBuilding(baseBuilder.Info.ProductionTypes, buildableThings);
|
||||
if (production != null && HasSufficientPowerForActor(production))
|
||||
{
|
||||
AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name);
|
||||
@@ -256,11 +253,11 @@ namespace OpenRA.Mods.Common.AI
|
||||
}
|
||||
|
||||
// Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings
|
||||
if (waterState == Water.EnoughWater && ai.Info.NewProductionCashThreshold > 0
|
||||
&& playerResources.Resources > ai.Info.NewProductionCashThreshold
|
||||
&& AIUtils.IsAreaAvailable<GivesBuildableArea>(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes))
|
||||
if (waterState == WaterCheck.EnoughWater && baseBuilder.Info.NewProductionCashThreshold > 0
|
||||
&& playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold
|
||||
&& AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes))
|
||||
{
|
||||
var navalproduction = GetProducibleBuilding(ai.Info.BuildingCommonNames.NavalProduction, buildableThings);
|
||||
var navalproduction = GetProducibleBuilding(baseBuilder.Info.NavalProductionTypes, buildableThings);
|
||||
if (navalproduction != null && HasSufficientPowerForActor(navalproduction))
|
||||
{
|
||||
AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name);
|
||||
@@ -277,7 +274,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
// Create some head room for resource storage if we really need it
|
||||
if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity)
|
||||
{
|
||||
var silo = GetProducibleBuilding(ai.Info.BuildingCommonNames.Silo, buildableThings);
|
||||
var silo = GetProducibleBuilding(baseBuilder.Info.SiloTypes, buildableThings);
|
||||
if (silo != null && HasSufficientPowerForActor(silo))
|
||||
{
|
||||
AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name);
|
||||
@@ -292,7 +289,7 @@ namespace OpenRA.Mods.Common.AI
|
||||
}
|
||||
|
||||
// Build everything else
|
||||
foreach (var frac in ai.Info.BuildingFractions.Shuffle(ai.Random))
|
||||
foreach (var frac in baseBuilder.Info.BuildingFractions.Shuffle(world.LocalRandom))
|
||||
{
|
||||
var name = frac.Key;
|
||||
|
||||
@@ -302,18 +299,18 @@ namespace OpenRA.Mods.Common.AI
|
||||
|
||||
// Do we want to build this structure?
|
||||
var count = playerBuildings.Count(a => a.Info.Name == name);
|
||||
if (count > frac.Value * playerBuildings.Length)
|
||||
if (count * 100 > frac.Value * playerBuildings.Length)
|
||||
continue;
|
||||
|
||||
if (ai.Info.BuildingLimits.ContainsKey(name) && ai.Info.BuildingLimits[name] <= count)
|
||||
if (baseBuilder.Info.BuildingLimits.ContainsKey(name) && baseBuilder.Info.BuildingLimits[name] <= count)
|
||||
continue;
|
||||
|
||||
// If we're considering to build a naval structure, check whether there is enough water inside the base perimeter
|
||||
// and any structure providing buildable area close enough to that water.
|
||||
// TODO: Extend this check to cover any naval structure, not just production.
|
||||
if (ai.Info.BuildingCommonNames.NavalProduction.Contains(name)
|
||||
&& (waterState == Water.NotEnoughWater
|
||||
|| !AIUtils.IsAreaAvailable<GivesBuildableArea>(ai.World, ai.Player, ai.Map, ai.Info.CheckForWaterRadius, ai.Info.WaterTerrainTypes)))
|
||||
if (baseBuilder.Info.NavalProductionTypes.Contains(name)
|
||||
&& (waterState == WaterCheck.NotEnoughWater
|
||||
|| !AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes)))
|
||||
continue;
|
||||
|
||||
// Will this put us into low power?
|
||||
@@ -342,5 +339,97 @@ namespace OpenRA.Mods.Common.AI
|
||||
// AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group);
|
||||
return null;
|
||||
}
|
||||
|
||||
CPos? ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type)
|
||||
{
|
||||
var actorInfo = world.Map.Rules.Actors[actorType];
|
||||
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
|
||||
if (bi == null)
|
||||
return null;
|
||||
|
||||
// Find the buildable cell that is closest to pos and centered around center
|
||||
Func<CPos, CPos, int, int, CPos?> findPos = (center, target, minRange, maxRange) =>
|
||||
{
|
||||
var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange);
|
||||
|
||||
// Sort by distance to target if we have one
|
||||
if (center != target)
|
||||
cells = cells.OrderBy(c => (c - target).LengthSquared);
|
||||
else
|
||||
cells = cells.Shuffle(world.LocalRandom);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (!world.CanPlaceBuilding(cell, actorInfo, bi, null))
|
||||
continue;
|
||||
|
||||
if (distanceToBaseIsImportant && !bi.IsCloseEnoughToBase(world, player, actorInfo, cell))
|
||||
continue;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
var baseCenter = baseBuilder.GetRandomBaseCenter();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case BuildingType.Defense:
|
||||
|
||||
// Build near the closest enemy structure
|
||||
var closestEnemy = world.ActorsHavingTrait<Building>().Where(a => !a.Disposed && player.Stances[a.Owner] == Stance.Enemy)
|
||||
.ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
|
||||
|
||||
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
|
||||
return findPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius);
|
||||
|
||||
case BuildingType.Refinery:
|
||||
|
||||
// Try and place the refinery near a resource field
|
||||
var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius)
|
||||
.Where(a => resourceTypeIndices.Get(world.Map.GetTerrainIndex(a)))
|
||||
.Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck);
|
||||
|
||||
foreach (var r in nearbyResources)
|
||||
{
|
||||
var found = findPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
// Try and find a free spot somewhere else in the base
|
||||
return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
|
||||
|
||||
case BuildingType.Building:
|
||||
return findPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius,
|
||||
distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange);
|
||||
}
|
||||
|
||||
// Can't find a build location
|
||||
return null;
|
||||
}
|
||||
|
||||
bool HasAdequateRefineryCount
|
||||
{
|
||||
get
|
||||
{
|
||||
// Require at least one refinery, unless we can't build it.
|
||||
return AIUtils.CountBuildingByCommonName(baseBuilder.Info.RefineryTypes, player) >= MinimumRefineryCount ||
|
||||
AIUtils.CountBuildingByCommonName(baseBuilder.Info.PowerTypes, player) == 0 ||
|
||||
AIUtils.CountBuildingByCommonName(baseBuilder.Info.ConstructionYardTypes, player) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int MinimumRefineryCount
|
||||
{
|
||||
get
|
||||
{
|
||||
// Unless we have no barracks (higher priority), require a 2nd refinery.
|
||||
// TODO: Possibly unhardcode this, at least the targeted minimum of 2 (the fallback can probably stay at 1).
|
||||
return AIUtils.CountBuildingByCommonName(baseBuilder.Info.BarracksTypes, player) > 0 ? 2 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2018 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 OpenRA.Mods.Common.AI;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[Desc("Manages AI repairing base buildings.")]
|
||||
public class BuildingRepairBotModuleInfo : ConditionalTraitInfo
|
||||
{
|
||||
public override object Create(ActorInitializer init) { return new BuildingRepairBotModule(init.Self, this); }
|
||||
}
|
||||
|
||||
public class BuildingRepairBotModule : ConditionalTrait<BuildingRepairBotModuleInfo>, IBotRespondToAttack
|
||||
{
|
||||
public BuildingRepairBotModule(Actor self, BuildingRepairBotModuleInfo info)
|
||||
: base(info) { }
|
||||
|
||||
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
||||
{
|
||||
var rb = self.TraitOrDefault<RepairableBuilding>();
|
||||
if (rb != null)
|
||||
{
|
||||
if (e.DamageState > DamageState.Light && e.PreviousDamageState <= DamageState.Light && !rb.RepairActive)
|
||||
{
|
||||
AIUtils.BotDebug("Bot noticed damage {0} {1}->{2}, repairing.",
|
||||
self, e.PreviousDamageState, e.DamageState);
|
||||
bot.QueueOrder(new Order("RepairBuilding", self.Owner.PlayerActor, Target.FromActor(self), false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,6 +452,15 @@ namespace OpenRA.Mods.Common.Traits
|
||||
[RequireExplicitImplementation]
|
||||
public interface IBotTick { void BotTick(IBot bot); }
|
||||
|
||||
public interface IBotRespondToAttack { void RespondToAttack(IBot bot, Actor self, AttackInfo e); }
|
||||
|
||||
[RequireExplicitImplementation]
|
||||
public interface IBotPositionsUpdated
|
||||
{
|
||||
void UpdatedBaseCenter(CPos newLocation);
|
||||
void UpdatedDefenseCenter(CPos newLocation);
|
||||
}
|
||||
|
||||
[RequireExplicitImplementation]
|
||||
public interface IEditorActorOptions : ITraitInfoInterface
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user