Bot mcv expansion: 1. Add ResourceMapBotModule. 2. Add McvExpansionManagerBotModule. 3. Give BaseBuilderBotModule better control over refinery. -- allow bot sell refinery too close. -- allow bot sell refinery far away from resource. (requires ResourceMapBotModule) -- allow bot recieve refinery construct requirement. 4. Give BaseBuilderBotModule control of expansion timing. (requires McvExpansionManagerBotModule) 5. Remove unused VehiclesFactoryTypes and useless BarracksTypes (now all use ProductionTypes). 6. Give HarvesterBotModule ability of order harvesters in low resource area to high resource area.

This commit is contained in:
dnqbob
2025-08-26 00:37:04 +08:00
committed by Gustas Kažukauskas
parent 7a2e25af86
commit 961702d818
13 changed files with 1876 additions and 179 deletions

View File

@@ -18,26 +18,23 @@ namespace OpenRA.Mods.Common.Traits
{
[TraitLocation(SystemActors.Player)]
[Desc("Manages AI base construction.")]
public class BaseBuilderBotModuleInfo : ConditionalTraitInfo
public class BaseBuilderBotModuleInfo : ConditionalTraitInfo, NotBefore<ResourceMapBotModuleInfo>, NotBefore<IResourceLayerInfo>
{
[Desc("Tells the AI what building types are considered construction yards.")]
public readonly HashSet<string> ConstructionYardTypes = [];
[Desc("Tells the AI what building types are considered vehicle production facilities.")]
public readonly HashSet<string> VehiclesFactoryTypes = [];
[Desc("Tells the AI what building types are considered refineries.")]
public readonly HashSet<string> RefineryTypes = [];
[Desc("Tells the AI what building types are considered power plants.")]
public readonly HashSet<string> PowerTypes = [];
[Desc("Tells the AI what building types are considered infantry production facilities.")]
public readonly HashSet<string> BarracksTypes = [];
[Desc("Tells the AI what building types are considered production facilities.")]
public readonly HashSet<string> ProductionTypes = [];
[Desc("Tells the AI what building types are considered tech buildings.")]
public readonly HashSet<string> TechTypes = [];
[Desc("Tells the AI what building types are considered naval production facilities.")]
public readonly HashSet<string> NavalProductionTypes = [];
@@ -71,10 +68,10 @@ namespace OpenRA.Mods.Common.Traits
[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.")]
[Desc("Number of refineries to build before building any production building.")]
public readonly int InititalMinimumRefineryCount = 1;
[Desc("Number of refineries to build additionally after building a barracks.")]
[Desc("Number of refineries to build additionally after building any production building.")]
public readonly int AdditionalMinimumRefineryCount = 1;
[Desc("Additional delay (in ticks) between structure production checks when there is no active production.",
@@ -139,15 +136,36 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Delay (in ticks) between reassigning rally points.")]
public readonly int AssignRallyPointsInterval = 100;
[Desc("Delay (in ticks) for finding a good resource to place a refinery next to.")]
public readonly int CheckBestResourceLocationInterval = 151;
[Desc("Interval (in ticks) between checking whether to sell a redundant refinery. Set to -1 to disable.")]
public readonly int SellRefineryInterval = 5000;
[Desc("Distance (in cells) for refineries finding redundant refineries.")]
public readonly int SellRefineryTooCloseCellDistance = 6;
[Desc("Maximum distance (in cells) from resources before refineries are eligible to be sold.")]
public readonly int SellRefineryNoResourceDistance = 12;
[Desc("Maximum refinery count per area. Area size is defined in " + nameof(ResourceMapBotModule) + ".")]
public readonly int MaxRefineryPerIndice = 2;
[Desc($"AI will move mcv when those numbers of refinery <= productions + tech - {nameof(ExpansionTolerate)}.")]
public readonly int[] ExpansionTolerate = [0, 1];
[Desc($"AI will move the only mcv when those numbers of refinery <= productions + tech - {nameof(ForceExpansionTolerate)}.")]
public readonly int[] ForceExpansionTolerate = [2, 3];
public override object Create(ActorInitializer init) { return new BaseBuilderBotModule(init.Self, this); }
}
public class BaseBuilderBotModule : ConditionalTrait<BaseBuilderBotModuleInfo>, IGameSaveTraitData,
IBotTick, IBotPositionsUpdated, IBotRespondToAttack, IBotRequestPauseUnitProduction, INotifyActorDisposing
IBotTick, IBotPositionsUpdated, IBotRespondToAttack, IBotRequestPauseUnitProduction, IBotSuggestRefineryProduction, INotifyActorDisposing
{
public CPos GetRandomBaseCenter()
{
var randomConstructionYard = constructionYardBuildings.Actors
var randomConstructionYard = ConstructionYardBuildings.Actors
.RandomOrDefault(world.LocalRandom);
return randomConstructionYard?.Location ?? initialBaseCenter;
@@ -157,6 +175,8 @@ namespace OpenRA.Mods.Common.Traits
// Actor, ActorCount.
public Dictionary<string, int> BuildingsBeingProduced = [];
public IBotBaseExpansion[] BaseExpansionModules;
public ResourceMapBotModule ResourceMapModule;
readonly World world;
readonly Player player;
@@ -166,17 +186,22 @@ namespace OpenRA.Mods.Common.Traits
IPathFinder pathFinder;
IBotPositionsUpdated[] positionsUpdatedModules;
CPos initialBaseCenter;
public CPos? ResourceConyardCenter;
public Dictionary<Actor, (CPos ConyardLoc, CPos ResourceLoc)> RequestedRefineries = [];
readonly Stack<TraitPair<RallyPoint>> rallyPoints = [];
int assignRallyPointsTicks;
int checkBestResourceLocationTicks;
int sellRefineryTick;
bool firstTick = true;
readonly BaseBuilderQueueManager[] builders;
int currentBuilderIndex = 0;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> refineryBuildings;
public readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> RefineryBuildings;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> powerBuildings;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> constructionYardBuildings;
readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> barracksBuildings;
public readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> ConstructionYardBuildings;
public readonly ActorIndex.OwnerAndNamesAndTrait<BuildingInfo> ProductionBuildings;
public BaseBuilderBotModule(Actor self, BaseBuilderBotModuleInfo info)
: base(info)
@@ -184,10 +209,10 @@ namespace OpenRA.Mods.Common.Traits
world = self.World;
player = self.Owner;
builders = new BaseBuilderQueueManager[info.BuildingQueues.Count + info.DefenseQueues.Count];
refineryBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.RefineryTypes, player);
RefineryBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.RefineryTypes, player);
powerBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.PowerTypes, player);
constructionYardBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.ConstructionYardTypes, player);
barracksBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.BarracksTypes, player);
ConstructionYardBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.ConstructionYardTypes, player);
ProductionBuildings = new ActorIndex.OwnerAndNamesAndTrait<BuildingInfo>(world, info.ProductionTypes, player);
}
protected override void Created(Actor self)
@@ -197,6 +222,7 @@ namespace OpenRA.Mods.Common.Traits
resourceLayer = self.World.WorldActor.TraitOrDefault<IResourceLayer>();
pathFinder = self.World.WorldActor.TraitOrDefault<IPathFinder>();
positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing<IBotPositionsUpdated>().ToArray();
BaseExpansionModules = self.Owner.PlayerActor.TraitsImplementing<IBotBaseExpansion>().ToArray();
var i = 0;
@@ -211,6 +237,8 @@ namespace OpenRA.Mods.Common.Traits
{
// Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay.
assignRallyPointsTicks = world.LocalRandom.Next(0, Info.AssignRallyPointsInterval);
checkBestResourceLocationTicks = world.LocalRandom.Next(0, Info.CheckBestResourceLocationInterval);
sellRefineryTick = Info.SellRefineryInterval < 0 ? 0 : world.LocalRandom.Next(0, Info.SellRefineryInterval);
}
void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation)
@@ -227,6 +255,12 @@ namespace OpenRA.Mods.Common.Traits
void IBotTick.BotTick(IBot bot)
{
if (firstTick)
{
ResourceMapModule = bot.Player.PlayerActor.TraitsImplementing<ResourceMapBotModule>().First(t => t.IsTraitEnabled());
firstTick = false;
}
if (--assignRallyPointsTicks <= 0)
{
assignRallyPointsTicks = Math.Max(2, Info.AssignRallyPointsInterval);
@@ -245,6 +279,51 @@ namespace OpenRA.Mods.Common.Traits
}
}
if (--checkBestResourceLocationTicks <= 0 && resourceLayer != null)
{
checkBestResourceLocationTicks = Info.CheckBestResourceLocationInterval;
// Clear outdated refinery requests that add too many refinery to a map indice
if (ResourceMapModule != null)
{
foreach (var mcv in RequestedRefineries.Keys.ToList())
{
if (ResourceMapModule.FindClosestIndiceFromCPos(
RequestedRefineries[mcv].ResourceLoc).PlayerRefineryCount >= Info.MaxRefineryPerIndice)
RequestedRefineries.Remove(mcv);
}
}
Actor bestconyard = null;
var best = int.MinValue;
foreach (var conyard in ConstructionYardBuildings.Actors)
{
if (conyard.IsDead)
continue;
if (!world.Map.FindTilesInAnnulus(conyard.Location, Info.MinBaseRadius, Info.MaxBaseRadius)
.Any(c => ResourceMapModule != null
? ResourceMapModule.Info.ValuableResourceTypes.Contains(resourceLayer.GetResource(c).Type)
: resourceLayer.GetResource(c).Type != null))
continue;
var refs = world.FindActorsInCircle(conyard.CenterPosition, WDist.FromCells(Info.MaxBaseRadius))
.Count(a => a.Owner == player && Info.RefineryTypes.Contains(a.Info.Name));
var suitable = -world.FindActorsInCircle(conyard.CenterPosition, WDist.FromCells(Info.MaxBaseRadius))
.Count(a => a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy) - refs;
if (suitable > best)
{
best = suitable;
bestconyard = conyard;
}
}
ResourceConyardCenter = bestconyard?.Location;
}
BuildingsBeingProduced.Clear();
// PERF: We tick only one type of valid queue at a time
@@ -286,6 +365,12 @@ namespace OpenRA.Mods.Common.Traits
}
builders[currentBuilderIndex].Tick(bot, queuesByCategory);
if (Info.SellRefineryInterval >= 0 && --sellRefineryTick <= 0)
{
SellUselessRefinery(bot);
sellRefineryTick = Info.SellRefineryInterval;
}
}
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
@@ -392,12 +477,12 @@ namespace OpenRA.Mods.Common.Traits
// Require at least one refinery, unless we can't build it.
public bool HasAdequateRefineryCount() =>
Info.RefineryTypes.Count == 0 ||
AIUtils.CountActorByCommonName(refineryBuildings) >= MinimumRefineryCount() ||
AIUtils.CountActorByCommonName(RefineryBuildings) >= MinimumRefineryCount() ||
AIUtils.CountActorByCommonName(powerBuildings) == 0 ||
AIUtils.CountActorByCommonName(constructionYardBuildings) == 0;
AIUtils.CountActorByCommonName(ConstructionYardBuildings) == 0;
int MinimumRefineryCount() =>
AIUtils.CountActorByCommonName(barracksBuildings) > 0
AIUtils.CountActorByCommonName(ProductionBuildings) > 0
? Info.InititalMinimumRefineryCount + Info.AdditionalMinimumRefineryCount
: Info.InititalMinimumRefineryCount;
@@ -413,6 +498,37 @@ namespace OpenRA.Mods.Common.Traits
];
}
void SellUselessRefinery(IBot bot)
{
// Sell one refinery each time. Perserve at least one refinery
var refineries = world.ActorsHavingTrait<Refinery>().Where(a => a.Owner == player).ToArray();
if (refineries.Length <= Info.InititalMinimumRefineryCount + Info.AdditionalMinimumRefineryCount)
return;
for (var i = 0; i < refineries.Length; i++)
{
for (var j = i + 1; j < refineries.Length; j++)
{
if ((refineries[i].Location - refineries[j].Location).LengthSquared <= Info.SellRefineryTooCloseCellDistance * Info.SellRefineryTooCloseCellDistance)
{
bot.QueueOrder(new Order("Sell", refineries[i], Target.FromActor(refineries[i]), false));
return;
}
}
if (ResourceMapModule != null &&
!world.Map.FindTilesInAnnulus(refineries[i].Location, 0, Info.SellRefineryNoResourceDistance)
.Any(c => ResourceMapModule.Info.ValuableResourceTypes.Contains(resourceLayer.GetResource(c).Type))
&& !world.FindActorsInCircle(refineries[i].CenterPosition, WDist.FromCells(Info.SellRefineryNoResourceDistance))
.Any(a => ResourceMapModule.Info.ResourceCreatorTypes.Contains(a.Info.Name)))
{
bot.QueueOrder(new Order("Sell", refineries[i], Target.FromActor(refineries[i]), false));
return;
}
}
}
void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data)
{
if (self.World.IsReplay)
@@ -429,10 +545,16 @@ namespace OpenRA.Mods.Common.Traits
void INotifyActorDisposing.Disposing(Actor self)
{
refineryBuildings.Dispose();
RefineryBuildings.Dispose();
powerBuildings.Dispose();
constructionYardBuildings.Dispose();
barracksBuildings.Dispose();
ConstructionYardBuildings.Dispose();
ProductionBuildings.Dispose();
}
void IBotSuggestRefineryProduction.RequestLocation(CPos refineryLocation, CPos conyardLocation, Actor expandActor)
{
if (ResourceMapModule == null || ResourceMapModule.FindClosestIndiceFromCPos(refineryLocation).PlayerRefineryCount < Info.MaxRefineryPerIndice)
RequestedRefineries[expandActor] = (conyardLocation, refineryLocation);
}
}
}