343 lines
13 KiB
C#
343 lines
13 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
[TraitLocation(SystemActors.Player)]
|
|
[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();
|
|
|
|
[Desc("Tells the AI what building types are considered vehicle production facilities.")]
|
|
public readonly HashSet<string> VehiclesFactoryTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered refineries.")]
|
|
public readonly HashSet<string> RefineryTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered power plants.")]
|
|
public readonly HashSet<string> PowerTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered infantry production facilities.")]
|
|
public readonly HashSet<string> BarracksTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered production facilities.")]
|
|
public readonly HashSet<string> ProductionTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered naval production facilities.")]
|
|
public readonly HashSet<string> NavalProductionTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered silos (resource storage).")]
|
|
public readonly HashSet<string> SiloTypes = new();
|
|
|
|
[Desc("Tells the AI what building types are considered defenses.")]
|
|
public readonly HashSet<string> DefenseTypes = new();
|
|
|
|
[Desc("Production queues AI uses for buildings.")]
|
|
public readonly HashSet<string> BuildingQueues = new() { "Building" };
|
|
|
|
[Desc("Production queues AI uses for defenses.")]
|
|
public readonly HashSet<string> DefenseQueues = new() { "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("Number of refineries to build before building a barracks.")]
|
|
public readonly int InititalMinimumRefineryCount = 1;
|
|
|
|
[Desc("Number of refineries to build additionally after building a barracks.")]
|
|
public readonly int AdditionalMinimumRefineryCount = 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: this should be at least as large as the typical order latency to avoid duplicated build choices.")]
|
|
public readonly int StructureProductionActiveDelay = 25;
|
|
|
|
[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("Chance that the AI will place the defenses in the direction of the closest enemy building.")]
|
|
public readonly int PlaceDefenseTowardsEnemyChance = 100;
|
|
|
|
[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() { "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;
|
|
|
|
[Desc("When should the AI start building specific buildings.")]
|
|
public readonly Dictionary<string, int> BuildingDelays = null;
|
|
|
|
[Desc("Only queue construction of a new structure when above this requirement.")]
|
|
public readonly int ProductionMinCashRequirement = 500;
|
|
|
|
public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); }
|
|
}
|
|
|
|
public class BaseBuilderBotModule : ConditionalTrait<BaseBuilderBotModuleInfo>, IGameSaveTraitData,
|
|
IBotTick, IBotPositionsUpdated, IBotRespondToAttack, IBotRequestPauseUnitProduction
|
|
{
|
|
public CPos GetRandomBaseCenter()
|
|
{
|
|
var randomConstructionYard = world.Actors.Where(a => a.Owner == player &&
|
|
Info.ConstructionYardTypes.Contains(a.Info.Name))
|
|
.RandomOrDefault(world.LocalRandom);
|
|
|
|
return randomConstructionYard?.Location ?? initialBaseCenter;
|
|
}
|
|
|
|
public CPos DefenseCenter { get; private set; }
|
|
|
|
// Actor, ActorCount.
|
|
public Dictionary<string, int> BuildingsBeingProduced = new();
|
|
|
|
readonly World world;
|
|
readonly Player player;
|
|
PowerManager playerPower;
|
|
PlayerResources playerResources;
|
|
IResourceLayer resourceLayer;
|
|
IBotPositionsUpdated[] positionsUpdatedModules;
|
|
CPos initialBaseCenter;
|
|
|
|
readonly BaseBuilderQueueManager[] builders;
|
|
int currentBuilderIndex = 0;
|
|
|
|
public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info)
|
|
: base(info)
|
|
{
|
|
world = self.World;
|
|
player = self.Owner;
|
|
builders = new BaseBuilderQueueManager[info.BuildingQueues.Count + info.DefenseQueues.Count];
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
playerPower = self.Owner.PlayerActor.TraitOrDefault<PowerManager>();
|
|
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
|
|
resourceLayer = self.World.WorldActor.TraitOrDefault<IResourceLayer>();
|
|
positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
|
|
|
|
var i = 0;
|
|
|
|
foreach (var building in Info.BuildingQueues)
|
|
builders[i++] = new BaseBuilderQueueManager(this, building, player, playerPower, playerResources, resourceLayer);
|
|
|
|
foreach (var defense in Info.DefenseQueues)
|
|
builders[i++] = new BaseBuilderQueueManager(this, defense, player, playerPower, playerResources, resourceLayer);
|
|
}
|
|
|
|
void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)
|
|
{
|
|
initialBaseCenter = newLocation;
|
|
}
|
|
|
|
void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation)
|
|
{
|
|
DefenseCenter = newLocation;
|
|
}
|
|
|
|
bool IBotRequestPauseUnitProduction.PauseUnitProduction => !IsTraitDisabled && !HasAdequateRefineryCount;
|
|
|
|
void IBotTick.BotTick(IBot bot)
|
|
{
|
|
// TODO: this causes pathfinding lag when AI's gets blocked in
|
|
SetRallyPointsForNewProductionBuildings(bot);
|
|
|
|
BuildingsBeingProduced.Clear();
|
|
|
|
// PERF: We tick only one type of valid queue at a time
|
|
// if AI gets enough cash, it can fill all of its queues with enough ticks
|
|
var findQueue = false;
|
|
for (int i = 0, builderIndex = currentBuilderIndex; i < builders.Length; i++)
|
|
{
|
|
if (++builderIndex >= builders.Length)
|
|
builderIndex = 0;
|
|
|
|
--builders[builderIndex].WaitTicks;
|
|
|
|
var queues = AIUtils.FindQueues(player, builders[builderIndex].Category).ToArray();
|
|
if (queues.Length != 0)
|
|
{
|
|
if (!findQueue)
|
|
{
|
|
currentBuilderIndex = builderIndex;
|
|
findQueue = true;
|
|
}
|
|
|
|
// Refresh "BuildingsBeingProduced" only when AI can produce
|
|
if (playerResources.GetCashAndResources() >= Info.ProductionMinCashRequirement)
|
|
{
|
|
foreach (var queue in queues)
|
|
{
|
|
var producing = queue.AllQueued().FirstOrDefault();
|
|
if (producing == null)
|
|
continue;
|
|
|
|
if (BuildingsBeingProduced.TryGetValue(producing.Item, out var number))
|
|
BuildingsBeingProduced[producing.Item] = number + 1;
|
|
else
|
|
BuildingsBeingProduced.Add(producing.Item, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
builders[currentBuilderIndex].Tick(bot);
|
|
}
|
|
|
|
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
|
{
|
|
if (e.Attacker == null || e.Attacker.Disposed)
|
|
return;
|
|
|
|
if (e.Attacker.Owner.RelationshipWith(self.Owner) != PlayerRelationship.Enemy)
|
|
return;
|
|
|
|
if (!e.Attacker.Info.HasTraitInfo<ITargetableInfo>())
|
|
return;
|
|
|
|
// Protect buildings
|
|
if (self.Info.HasTraitInfo<BuildingInfo>())
|
|
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)
|
|
continue;
|
|
|
|
if (rp.Trait.Path.Count == 0 || !IsRallyPointValid(rp.Trait.Path[0], 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>()))
|
|
.ToList();
|
|
|
|
if (possibleRallyPoints.Count == 0)
|
|
{
|
|
AIUtils.BotDebug("{0} has no possible rallypoint near {1}", producer.Owner, producer.Location);
|
|
return producer.Location;
|
|
}
|
|
|
|
return possibleRallyPoints.Random(world.LocalRandom);
|
|
}
|
|
|
|
bool IsRallyPointValid(CPos x, BuildingInfo info)
|
|
{
|
|
return info != null && world.IsCellBuildable(x, null, info);
|
|
}
|
|
|
|
// Require at least one refinery, unless we can't build it.
|
|
public bool HasAdequateRefineryCount =>
|
|
Info.RefineryTypes.Count == 0 ||
|
|
AIUtils.CountBuildingByCommonName(Info.RefineryTypes, player) >= MinimumRefineryCount ||
|
|
AIUtils.CountBuildingByCommonName(Info.PowerTypes, player) == 0 ||
|
|
AIUtils.CountBuildingByCommonName(Info.ConstructionYardTypes, player) == 0;
|
|
|
|
int MinimumRefineryCount => AIUtils.CountBuildingByCommonName(Info.BarracksTypes, player) > 0 ? Info.InititalMinimumRefineryCount + Info.AdditionalMinimumRefineryCount : Info.InititalMinimumRefineryCount;
|
|
|
|
List<MiniYamlNode> IGameSaveTraitData.IssueTraitData(Actor self)
|
|
{
|
|
if (IsTraitDisabled)
|
|
return null;
|
|
|
|
return new List<MiniYamlNode>()
|
|
{
|
|
new("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)),
|
|
new("DefenseCenter", FieldSaver.FormatValue(DefenseCenter))
|
|
};
|
|
}
|
|
|
|
void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data)
|
|
{
|
|
if (self.World.IsReplay)
|
|
return;
|
|
|
|
var initialBaseCenterNode = data.NodeWithKeyOrDefault("InitialBaseCenter");
|
|
if (initialBaseCenterNode != null)
|
|
initialBaseCenter = FieldLoader.GetValue<CPos>("InitialBaseCenter", initialBaseCenterNode.Value.Value);
|
|
|
|
var defenseCenterNode = data.NodeWithKeyOrDefault("DefenseCenter");
|
|
if (defenseCenterNode != null)
|
|
DefenseCenter = FieldLoader.GetValue<CPos>("DefenseCenter", defenseCenterNode.Value.Value);
|
|
}
|
|
}
|
|
}
|