Extract BaseBuilderBotModule from HackyAI

This commit is contained in:
reaperrr
2018-11-05 02:39:48 +01:00
committed by Paul Chote
parent 9f30e2ecb0
commit 04c34741c8
7 changed files with 559 additions and 323 deletions

View 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);
}
}
}

View File

@@ -0,0 +1,435 @@
#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.Traits;
namespace OpenRA.Mods.Common.Traits
{
class BaseBuilderQueueManager
{
readonly string category;
readonly BaseBuilderBotModule baseBuilder;
readonly World world;
readonly Player player;
readonly PowerManager playerPower;
readonly PlayerResources playerResources;
int waitTicks;
Actor[] playerBuildings;
int failCount;
int failRetryTicks;
int checkForBasesTicks;
int cachedBases;
int cachedBuildings;
int minimumExcessPower;
BitArray resourceTypeIndices;
WaterCheck waterState = WaterCheck.NotChecked;
public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm,
PlayerResources pr, BitArray resourceTypeIndices)
{
this.baseBuilder = baseBuilder;
world = p.World;
player = p;
playerPower = pm;
playerResources = pr;
this.category = category;
failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay;
minimumExcessPower = baseBuilder.Info.MinimumExcessPower;
this.resourceTypeIndices = resourceTypeIndices;
}
public void Tick(IBot bot)
{
// If failed to place something N consecutive times, wait M ticks until resuming building production
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);
// Only bother resetting failCount if either a) the number of buildings has decreased since last failure M ticks ago,
// or b) number of BaseProviders (construction yard or similar) has increased since then.
// Otherwise reset failRetryTicks instead to wait again.
if (currentBuildings < cachedBuildings || baseProviders > cachedBases)
failCount = 0;
else
failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay;
}
if (waterState == WaterCheck.NotChecked)
{
if (AIUtils.IsAreaAvailable<BaseProvider>(world, player, world.Map, baseBuilder.Info.MaxBaseRadius, baseBuilder.Info.WaterTerrainTypes))
waterState = WaterCheck.EnoughWater;
else
{
waterState = WaterCheck.NotEnoughWater;
checkForBasesTicks = baseBuilder.Info.CheckForNewBasesDelay;
}
}
if (waterState == WaterCheck.NotEnoughWater && --checkForBasesTicks <= 0)
{
var currentBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
if (currentBases > cachedBases)
{
cachedBases = currentBases;
waterState = WaterCheck.NotChecked;
}
}
// Only update once per second or so
if (--waitTicks > 0)
return;
playerBuildings = world.ActorsHavingTrait<Building>().Where(a => a.Owner == player).ToArray();
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 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 = 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 + baseBuilder.Info.StructureProductionActiveDelay + randomFactor
: baseBuilder.Info.StructureProductionInactiveDelay + randomFactor;
}
bool TickQueue(IBot bot, ProductionQueue queue)
{
var currentBuilding = queue.AllQueued().FirstOrDefault();
// Waiting to build something
if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts)
{
var item = ChooseBuildingToBuild(queue);
if (item == null)
return false;
bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
}
else if (currentBuilding != null && currentBuilding.Done)
{
// Production is complete
// Choose the placement logic
// HACK: HACK HACK HACK
// TODO: Derive this from BuildingCommonNames instead
var type = BuildingType.Building;
if (world.Map.Rules.Actors[currentBuilding.Item].HasTraitInfo<AttackBaseInfo>())
type = BuildingType.Defense;
else if (baseBuilder.Info.RefineryTypes.Contains(world.Map.Rules.Actors[currentBuilding.Item].Name))
type = BuildingType.Refinery;
var location = ChooseBuildLocation(currentBuilding.Item, true, type);
if (location == null)
{
AIUtils.BotDebug("AI: {0} has nowhere to place {1}".F(player, currentBuilding.Item));
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 == baseBuilder.Info.MaximumFailedPlacementAttempts)
{
cachedBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
cachedBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
}
}
else
{
failCount = 0;
bot.QueueOrder(new Order("PlaceBuilding", player.PlayerActor, Target.FromCell(world, location.Value), false)
{
// Building to place
TargetString = currentBuilding.Item,
// Actor ID to associate the placement with
ExtraData = queue.Actor.ActorID,
SuppressVisualFeedback = true
});
return true;
}
}
return true;
}
ActorInfo GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null)
{
var available = buildables.Where(actor =>
{
// Are we able to build this?
if (!actors.Contains(actor.Name))
return false;
if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name))
return true;
return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name];
});
if (orderBy != null)
return available.MaxByOrDefault(orderBy);
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) >= baseBuilder.Info.MinimumExcessPower;
}
ActorInfo ChooseBuildingToBuild(ProductionQueue queue)
{
var buildableThings = queue.BuildableItems();
// This gets used quite a bit, so let's cache it here
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
if (playerPower != null && playerPower.ExcessPower < minimumExcessPower)
{
if (power != null && power.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0)
{
AIUtils.BotDebug("AI: {0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Next is to build up a strong economy
if (!HasAdequateRefineryCount)
{
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);
return refinery;
}
if (power != null && refinery != null && !HasSufficientPowerForActor(refinery))
{
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Make sure that we can spend as fast as we are earning
if (baseBuilder.Info.NewProductionCashThreshold > 0 && playerResources.Resources > baseBuilder.Info.NewProductionCashThreshold)
{
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);
return production;
}
if (power != null && production != null && !HasSufficientPowerForActor(production))
{
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings
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(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);
return navalproduction;
}
if (power != null && navalproduction != null && !HasSufficientPowerForActor(navalproduction))
{
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Create some head room for resource storage if we really need it
if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity)
{
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);
return silo;
}
if (power != null && silo != null && !HasSufficientPowerForActor(silo))
{
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Build everything else
foreach (var frac in baseBuilder.Info.BuildingFractions.Shuffle(world.LocalRandom))
{
var name = frac.Key;
// Can we build this structure?
if (!buildableThings.Any(b => b.Name == name))
continue;
// Do we want to build this structure?
var count = playerBuildings.Count(a => a.Info.Name == name);
if (count * 100 > frac.Value * playerBuildings.Length)
continue;
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 (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?
var actor = world.Map.Rules.Actors[name];
if (playerPower != null && (playerPower.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor)))
{
// Try building a power plant instead
if (power != null && power.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0)
{
if (playerPower.PowerOutageRemainingTicks > 0)
AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name);
else
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
return power;
}
}
// Lets build this
AIUtils.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}",
queue.Actor.Owner, name, frac.Value, frac.Value * playerBuildings.Length, playerBuildings.Length, count);
return actor;
}
// Too spammy to keep enabled all the time, but very useful when debugging specific issues.
// 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;
}
}
}
}

View File

@@ -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));
}
}
}
}
}