From 961702d818bc38f98951155ef426ec3bb060aa8b Mon Sep 17 00:00:00 2001 From: dnqbob Date: Tue, 26 Aug 2025 00:37:04 +0800 Subject: [PATCH] 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. --- .../Traits/BotModules/BaseBuilderBotModule.cs | 168 +++- .../BotModuleLogic/BaseBuilderQueueManager.cs | 128 ++- .../Traits/BotModules/HarvesterBotModule.cs | 225 ++++- .../McvExpansionManagerBotModule.cs | 824 ++++++++++++++++++ .../Traits/BotModules/ResourceMapBotModule.cs | 308 +++++++ OpenRA.Mods.Common/TraitsInterfaces.cs | 12 + ...sAndVehiclesTypesInBaseBuilderBotModule.cs | 32 + OpenRA.Mods.Common/UpdateRules/UpdatePath.cs | 1 + mods/cnc/rules/ai.yaml | 144 ++- mods/d2k/rules/ai.yaml | 6 +- mods/ra/maps/soviet-05/rules.yaml | 2 - mods/ra/rules/ai.yaml | 188 ++-- mods/ts/rules/ai.yaml | 17 +- 13 files changed, 1876 insertions(+), 179 deletions(-) create mode 100644 OpenRA.Mods.Common/Traits/BotModules/McvExpansionManagerBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/ResourceMapBotModule.cs create mode 100644 OpenRA.Mods.Common/UpdateRules/Rules/20250330/RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule.cs diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index 74ac56a2c3..bd3c057617 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -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, NotBefore { [Desc("Tells the AI what building types are considered construction yards.")] public readonly HashSet ConstructionYardTypes = []; - [Desc("Tells the AI what building types are considered vehicle production facilities.")] - public readonly HashSet VehiclesFactoryTypes = []; - [Desc("Tells the AI what building types are considered refineries.")] public readonly HashSet RefineryTypes = []; [Desc("Tells the AI what building types are considered power plants.")] public readonly HashSet PowerTypes = []; - [Desc("Tells the AI what building types are considered infantry production facilities.")] - public readonly HashSet BarracksTypes = []; - [Desc("Tells the AI what building types are considered production facilities.")] public readonly HashSet ProductionTypes = []; + [Desc("Tells the AI what building types are considered tech buildings.")] + public readonly HashSet TechTypes = []; + [Desc("Tells the AI what building types are considered naval production facilities.")] public readonly HashSet 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, 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 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 RequestedRefineries = []; readonly Stack> rallyPoints = []; int assignRallyPointsTicks; + int checkBestResourceLocationTicks; + int sellRefineryTick; + bool firstTick = true; readonly BaseBuilderQueueManager[] builders; int currentBuilderIndex = 0; - readonly ActorIndex.OwnerAndNamesAndTrait refineryBuildings; + public readonly ActorIndex.OwnerAndNamesAndTrait RefineryBuildings; readonly ActorIndex.OwnerAndNamesAndTrait powerBuildings; - readonly ActorIndex.OwnerAndNamesAndTrait constructionYardBuildings; - readonly ActorIndex.OwnerAndNamesAndTrait barracksBuildings; + public readonly ActorIndex.OwnerAndNamesAndTrait ConstructionYardBuildings; + public readonly ActorIndex.OwnerAndNamesAndTrait 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(world, info.RefineryTypes, player); + RefineryBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.RefineryTypes, player); powerBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.PowerTypes, player); - constructionYardBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.ConstructionYardTypes, player); - barracksBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.BarracksTypes, player); + ConstructionYardBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.ConstructionYardTypes, player); + ProductionBuildings = new ActorIndex.OwnerAndNamesAndTrait(world, info.ProductionTypes, player); } protected override void Created(Actor self) @@ -197,6 +222,7 @@ namespace OpenRA.Mods.Common.Traits resourceLayer = self.World.WorldActor.TraitOrDefault(); pathFinder = self.World.WorldActor.TraitOrDefault(); positionsUpdatedModules = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + BaseExpansionModules = self.Owner.PlayerActor.TraitsImplementing().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().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().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); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index e55242997f..93267dabdd 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -35,6 +35,7 @@ namespace OpenRA.Mods.Common.Traits int cachedBases; int cachedBuildings; int minimumExcessPower; + CPos? baseCenterKeepsFailing = null; bool itemQueuedThisTick = false; @@ -50,7 +51,6 @@ namespace OpenRA.Mods.Common.Traits playerResources = pr; resourceLayer = rl; Category = category; - failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; minimumExcessPower = baseBuilder.Info.MinimumExcessPower; if (baseBuilder.Info.NavalProductionTypes.Count == 0) waterState = WaterCheck.DontCheck; @@ -58,19 +58,41 @@ namespace OpenRA.Mods.Common.Traits public void Tick(IBot bot, ILookup queuesByCategory) { - // If failed to place something N consecutive times, wait M ticks until resuming building production - if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0) + // If failed to place something N consecutive times, we will try move the MCV + // If it is possible. + if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts) { - var currentBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); - var baseProviders = world.ActorsHavingTrait().Count(a => a.Owner == player); + if (baseBuilder.BaseExpansionModules != null && baseCenterKeepsFailing != null) + { + var stuckConyard = baseBuilder.ConstructionYardBuildings.Actors + .Where(a => (a.Location - baseCenterKeepsFailing.Value).LengthSquared <= baseBuilder.Info.MaxBaseRadius * baseBuilder.Info.MaxBaseRadius) + .MinByOrDefault(a => (a.Location - baseCenterKeepsFailing.Value).LengthSquared); - // Only bother resetting failCount if either a) the number of buildings has decreased since last failure M ticks ago, + if (stuckConyard != null) + { + foreach (var be in baseBuilder.BaseExpansionModules) + be.UpdateExpansionParams(bot, false, true, stuckConyard); + } + + failCount = 0; + } + + // Otherwise, 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; + else if (baseBuilder.BaseExpansionModules == null && --failRetryTicks <= 0) + { + var currentBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); + var baseProviders = world.ActorsHavingTrait().Count(a => a.Owner == player); + + if (currentBuildings < cachedBuildings || baseProviders > cachedBases) + failCount = 0; + else + failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay; + } + + if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts) + return; } if (waterState == WaterCheck.NotChecked) @@ -176,20 +198,21 @@ namespace OpenRA.Mods.Common.Traits else if (baseBuilder.Info.RefineryTypes.Contains(actorInfo.Name)) type = BuildingType.Refinery; - (location, actorVariant) = ChooseBuildLocation(currentBuilding.Item, true, type); + (location, baseCenterKeepsFailing, actorVariant) = ChooseBuildLocation(currentBuilding.Item, true, type); } if (location == null) { - AIUtils.BotDebug($"{player} has nowhere to place {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) + if (++failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts) { - cachedBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); - cachedBases = world.ActorsHavingTrait().Count(a => a.Owner == player); + AIUtils.BotDebug($"{player} has nowhere to place {currentBuilding.Item}"); + bot.QueueOrder(Order.CancelProduction(queue.Actor, currentBuilding.Item, 1)); + if (baseBuilder.BaseExpansionModules == null) + { + cachedBuildings = world.ActorsHavingTrait().Count(a => a.Owner == player); + cachedBases = world.ActorsHavingTrait().Count(a => a.Owner == player); + } } } else @@ -209,6 +232,26 @@ namespace OpenRA.Mods.Common.Traits SuppressVisualFeedback = true }); + if (baseBuilder.Info.ProductionTypes.Contains(currentBuilding.Item) + || baseBuilder.Info.TechTypes.Contains(currentBuilding.Item) || baseBuilder.Info.RefineryTypes.Contains(currentBuilding.Item)) + { + var numRef = baseBuilder.RefineryBuildings.Actors.Count(a => !a.IsDead) + (baseBuilder.Info.RefineryTypes.Contains(currentBuilding.Item) ? 1 : 0); + + var numProd = baseBuilder.ProductionBuildings.Actors.Count(a => !a.IsDead) + (baseBuilder.Info.ProductionTypes.Contains(currentBuilding.Item) ? 1 : 0); + + var numTech = playerBuildings.Count(a => baseBuilder.Info.TechTypes.Contains(a.Info.Name)) + + (baseBuilder.Info.TechTypes.Contains(currentBuilding.Item) ? 1 : 0); + + if (numRef >= baseBuilder.Info.InititalMinimumRefineryCount + baseBuilder.Info.AdditionalMinimumRefineryCount + && numProd > 0 && numProd - baseBuilder.Info.ExpansionTolerate.Random(world.LocalRandom) + numTech >= numRef) + { + var undeployEvenNoBase = numProd - baseBuilder.Info.ForceExpansionTolerate.Random(world.LocalRandom) + numTech >= numRef; + + foreach (var be in baseBuilder.BaseExpansionModules) + be.UpdateExpansionParams(bot, true, undeployEvenNoBase, null); + } + } + return true; } } @@ -259,7 +302,7 @@ namespace OpenRA.Mods.Common.Traits } // Next is to build up a strong economy - if (!baseBuilder.HasAdequateRefineryCount()) + if (baseBuilder.RequestedRefineries.Count > 0 || !baseBuilder.HasAdequateRefineryCount()) { var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) @@ -394,16 +437,16 @@ namespace OpenRA.Mods.Common.Traits return null; } - (CPos? Location, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) + (CPos? Location, CPos? BaseCenter, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) { var actorInfo = world.Map.Rules.Actors[actorType]; var bi = actorInfo.TraitInfoOrDefault(); if (bi == null) - return (null, 0); + return (null, null, 0); // Find the buildable cell that is closest to pos and centered around center - (CPos? Location, int Variant) FindPos(CPos center, CPos target, int minRange, int maxRange) + (CPos? Location, CPos Center, int Variant) FindPos(CPos center, CPos target, int minRange, int maxRange) { var actorVariant = 0; var buildingVariantInfo = actorInfo.TraitInfoOrDefault(); @@ -470,10 +513,10 @@ namespace OpenRA.Mods.Common.Traits if (distanceToBaseIsImportant && !vbi.IsCloseEnoughToBase(world, player, variantActorInfo, cell)) continue; - return (cell, actorVariant); + return (cell, center, actorVariant); } - return (null, 0); + return (null, center, 0); } var baseCenter = baseBuilder.GetRandomBaseCenter(); @@ -493,21 +536,46 @@ namespace OpenRA.Mods.Common.Traits case BuildingType.Refinery: + var requestRef = baseBuilder.RequestedRefineries.Count > 0 ? baseBuilder.RequestedRefineries.Keys.First() : null; + // Try and place the refinery near a resource field if (resourceLayer != null) { - var nearbyResources = world.Map.FindTilesInAnnulus(baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) - .Where(a => resourceLayer.GetResource(a).Type != null) - .Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck); + // If we have failed to place to the best refinery point, try and place it near the base center + var resourceBaseCenter = failCount > 0 ? baseCenter : (requestRef != null ? + baseBuilder.RequestedRefineries[requestRef].ConyardLoc : (baseBuilder.ResourceConyardCenter ?? baseCenter)); - foreach (var r in nearbyResources) + var nearbyResources = world.Map + .FindTilesInAnnulus(resourceBaseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius) + .Where(c => baseBuilder.ResourceMapModule != null ? + baseBuilder.ResourceMapModule.Info.ValuableResourceTypes.Contains(resourceLayer.GetResource(c).Type) + : resourceLayer.GetResource(c).Type != null); + + var closestRefinery = failCount <= 0 + ? baseBuilder.RefineryBuildings.Actors.Where(a => !a.IsDead)?.ClosestToIgnoringPath(world.Map.CenterOfCell(resourceBaseCenter)) + : null; + + var resourcesShouldCheck = closestRefinery == null ? + nearbyResources.Shuffle(world.LocalRandom) : + (requestRef != null ? nearbyResources.OrderBy(c => (c - baseBuilder.RequestedRefineries[requestRef].ResourceLoc).LengthSquared) + : nearbyResources.OrderByDescending(c => (c - closestRefinery.Location).LengthSquared)) + .Take(baseBuilder.Info.MaxResourceCellsToCheck); + + foreach (var r in resourcesShouldCheck) { - var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); + var found = FindPos(resourceBaseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); if (found.Location != null) + { + if (baseBuilder.RequestedRefineries.Count > 0) + baseBuilder.RequestedRefineries.Remove(requestRef); return found; + } } } + if (baseBuilder.RequestedRefineries.Count > 0) + baseBuilder.RequestedRefineries.Remove(requestRef); + // Try and find a free spot somewhere else in the base return FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius); @@ -517,7 +585,7 @@ namespace OpenRA.Mods.Common.Traits } // Can't find a build location - return (null, 0); + return (null, null, 0); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs index 21cd31fad3..c5afa9a385 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/HarvesterBotModule.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Traits { [TraitLocation(SystemActors.Player)] [Desc("Put this on the Player actor. Manages bot harvesters to ensure they always continue harvesting as long as there are resources on the map.")] - public class HarvesterBotModuleInfo : ConditionalTraitInfo, NotBefore + public class HarvesterBotModuleInfo : ConditionalTraitInfo, NotBefore, NotBefore { [Desc("Actor types that are considered harvesters. If harvester count drops below RefineryTypes count, a new harvester is built.", "Leave empty to disable harvester replacement. Currently only needed by harvester replacement system.")] @@ -32,6 +32,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Interval (in ticks) between giving out orders to idle harvesters.")] public readonly int ScanForIdleHarvestersInterval = 50; + [Desc("Interval (in ticks) between giving out orders to idle harvesters.")] + public readonly int ScanForLowEffectHarvestersInterval = 433; + [Desc("When an idle harvester cannot find resources, increase the wait to this many scan intervals.")] public readonly int ScanIntervalMultiplerWhenNoResources = 5; @@ -41,10 +44,16 @@ namespace OpenRA.Mods.Common.Traits [Desc("For each enemy within the threat radius, apply the following cost multiplier for every cell that needs to be moved through.")] public readonly int HarvesterEnemyAvoidanceCostMultipler = 20; + [Desc("How many resource cells should a harvester response for.")] + public readonly int ResourceCellsPerHarvester = 4; + + [Desc("How many harvester should player owned at least.")] + public readonly int InitialHarvesters = 4; + public override object Create(ActorInitializer init) { return new HarvesterBotModule(init.Self, this); } } - public class HarvesterBotModule : ConditionalTrait, IBotTick, INotifyActorDisposing, IWorldLoaded + public class HarvesterBotModule : ConditionalTrait, IBotTick, IBotRespondToAttack, INotifyActorDisposing, IWorldLoaded { sealed class HarvesterTraitWrapper { @@ -77,7 +86,12 @@ namespace OpenRA.Mods.Common.Traits IResourceLayer resourceLayer; ResourceClaimLayer claimLayer; IBotRequestUnitProduction[] requestUnitProduction; + ResourceMapBotModule resourceMapModule; + + int scanForLowEffectHarvestersTicks; int scanForIdleHarvestersTicks; + int respondToAttackCooldown = 40; // prevent too many responses to the same wave of attacks + bool firstTick = true; public HarvesterBotModule(Actor self, HarvesterBotModuleInfo info) : base(info) @@ -123,28 +137,194 @@ namespace OpenRA.Mods.Common.Traits { // Avoid all AIs scanning for idle harvesters on the same tick, randomize their initial scan delay. scanForIdleHarvestersTicks = world.LocalRandom.Next(Info.ScanForIdleHarvestersInterval); + scanForLowEffectHarvestersTicks = world.LocalRandom.Next(Info.ScanForLowEffectHarvestersInterval); } void IBotTick.BotTick(IBot bot) { + respondToAttackCooldown--; + if (resourceLayer == null || resourceLayer.IsEmpty) return; + if (firstTick) + { + resourceMapModule = bot.Player.PlayerActor.TraitsImplementing().First(t => t.IsTraitEnabled()); + firstTick = false; + } + // Find idle harvesters and give them orders: // PERF: FindNextResource is expensive, so only perform one search per tick. var searchedForResources = false; while (harvestersNeedingOrders.TryPop(out var hno) && !searchedForResources) searchedForResources = HarvestIfAble(bot, hno); - if (--scanForIdleHarvestersTicks > 0) + if (--scanForIdleHarvestersTicks <= 0) + { + scanForIdleHarvestersTicks = Info.ScanForIdleHarvestersInterval; + FindIdleHarvester(); + + // Less harvesters than refineries - build a new harvester + var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault(); + if (unitBuilder != null && Info.HarvesterTypes.Count > 0) + { + var harvsNum = AIUtils.CountActorByCommonName(harvestersIndex); + var harvCountTooLow = harvsNum < Info.InitialHarvesters || harvsNum < AIUtils.CountActorByCommonName(refineries); + if (harvCountTooLow) + { + var harvesterType = Info.HarvesterTypes.Random(world.LocalRandom); + if (unitBuilder.RequestedProductionCount(bot, harvesterType) == 0) + unitBuilder.RequestUnitProduction(bot, harvesterType); + } + } + } + + if (--scanForLowEffectHarvestersTicks <= 0) + { + scanForLowEffectHarvestersTicks = Info.ScanForLowEffectHarvestersInterval; + if (resourceMapModule != null) + FindAndOrderLowEffectHarvesterOnResourceMap(bot); + } + } + + void FindAndOrderLowEffectHarvesterOnResourceMap(IBot bot) + { + CPos? worstEffectIndice = null; + var worstEffectHarvesterCount = int.MaxValue; + + var lackHarvesterIndices = new List<(int Attraction, int LackHarvs, CPos ResoueceCenter)>(); + /* + * indiceSideLengthSquare (which is equal to indiceSideLength * indiceSideLength) is used as the basic unit to calculate the attraction of a candidate, + * we compare the attraction on the same scale on different factors, such as candidate's distance to current MCV and ally construction yard & refinery within range, etc: + */ + + var indiceSideLengthSquare = resourceMapModule.GetIndiceSideLength() * resourceMapModule.GetIndiceSideLength(); + + for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++) + { + var baseIndice = resourceMapModule.GetIndice(i); + + // Initial attraction is indiceSideLengthSquare >> 5 + var attraction = indiceSideLengthSquare >> 5; + + attraction += baseIndice.ResourceCellsCount - baseIndice.PlayerHarvetserCount * Info.ResourceCellsPerHarvester; + + var lackHarvs = attraction > 0 ? attraction / Info.ResourceCellsPerHarvester : (attraction == 0 && baseIndice.ResourceCellsCount > 0 ? 1 : -1); + + // Reduce the attraction of resource cells count + attraction >>= 1; + + if (baseIndice.PlayerRefineryCount <= 0 && lackHarvs > 0) + lackHarvs = 1; + + // If there is enemy in indice, reduce attraction by indiceSideLengthSquare << 4 + // If there is enemy in the nearby indices. reduce attraction by indiceSideLengthSquare >> 5 (equals to initial attraction) + if (baseIndice.EnemyBaseCount > 0 || baseIndice.EnemyUnitCount > 0) + attraction -= indiceSideLengthSquare << 4; + else + { + var (indiceCount, nearbyEnemyBase, nearbyEnemy) = resourceMapModule.GetNearbyIndicesThreat(i); + if (nearbyEnemyBase + nearbyEnemy > 0) + attraction -= indiceSideLengthSquare >> 5; + } + + if (baseIndice.PlayerRefineryCount > 0) + attraction += indiceSideLengthSquare; + + if (baseIndice.ResourceCellsCount > 0 && attraction > 0 && lackHarvs > 0) + lackHarvesterIndices.Add((attraction, lackHarvs, baseIndice.ResourceCellsCenter)); + + if (lackHarvs < worstEffectHarvesterCount && lackHarvs < 0) + { + worstEffectHarvesterCount = lackHarvs; + worstEffectIndice = baseIndice.IndiceCenter; + } + } + + if (worstEffectIndice == null) return; + var harvestersCanAssign = -worstEffectHarvesterCount; + + // Try to find a new resource patch for the worst effect harvester + var searchRadius = resourceMapModule.GetIndiceScanRadius(); + + var harvesters = world.FindActorsInCircle(world.Map.CenterOfCell(worstEffectIndice.Value), WDist.FromCells(searchRadius)) + .Where(a => a.Owner == player && resourceMapModule.Info.HarvesterTypes.Contains(a.Info.Name)).ToList(); + + var pathDistanceSquareFactor = resourceMapModule.GetIndiceRowCount() * resourceMapModule.GetIndiceRowCount() + + resourceMapModule.GetIndiceColumnCount() * resourceMapModule.GetIndiceColumnCount(); + + harvestersCanAssign = Math.Min(harvestersCanAssign, harvesters.Count - 1); + if (harvestersCanAssign > 0) + { + foreach (var (_, lackHarvs, resourceCenter) in + lackHarvesterIndices.OrderByDescending(d => d.Attraction - (harvesters[0].Location - d.ResoueceCenter).LengthSquared / pathDistanceSquareFactor)) + { + if (harvestersCanAssign <= 0) + break; + + var needHarvs = lackHarvs; + var nearbyResources = world.Map.FindTilesInAnnulus(resourceCenter, 0, resourceMapModule.GetIndiceScanRadius()) + .Where(c => resourceMapModule.Info.ValuableResourceTypes.Contains(resourceLayer.GetResource(c).Type) + && (harvesters[0].Location - resourceCenter).LengthSquared >= (c - harvesters[0].Location).LengthSquared).ToArray(); + + if (nearbyResources.Length <= 0 || needHarvs <= 0) + continue; + + var usedHarvs = new HashSet(); + foreach (var harv in harvesters) + { + if (needHarvs <= 0 || harvestersCanAssign <= 0) + break; + + var parach = harv.TraitOrDefault(); + if (parach != null && parach.IsInAir) + { + harvestersCanAssign--; + usedHarvs.Add(harv); + continue; + } + + var mobile = harv.TraitOrDefault(); + if (mobile != null) + { + var tcell = nearbyResources.Random(world.LocalRandom); + if (mobile.PathFinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, harv.Location, tcell)) + { + bot.QueueOrder(new Order("Harvest", harv, Target.FromCell(world, tcell), false)); + needHarvs--; + harvestersCanAssign--; + usedHarvs.Add(harv); + } + else + { + if (needHarvs > 1) + needHarvs = 1; + else + break; + } + } + else + { + bot.QueueOrder(new Order("Harvest", harv, Target.FromCell(world, nearbyResources.Random(world.LocalRandom)), false)); + needHarvs--; + harvestersCanAssign--; + usedHarvs.Add(harv); + } + } + + harvesters.RemoveAll(usedHarvs.Contains); + } + } + } + + void FindIdleHarvester() + { var toRemove = harvesters.Keys.Where(unitCannotBeOrdered).ToList(); foreach (var a in toRemove) harvesters.Remove(a); - scanForIdleHarvestersTicks = Info.ScanForIdleHarvestersInterval; - // Find new harvesters var newHarvesters = world.ActorsHavingTrait().Where(a => !unitCannotBeOrdered(a) && !harvesters.ContainsKey(a)); foreach (var a in newHarvesters) @@ -153,21 +333,6 @@ namespace OpenRA.Mods.Common.Traits harvestersNeedingOrders.Clear(); foreach (var h in harvesters) harvestersNeedingOrders.Push(h.Value); - - // Less harvesters than refineries - build a new harvester - var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault(); - if (unitBuilder != null && Info.HarvesterTypes.Count > 0) - { - var harvCountTooLow = - AIUtils.CountActorByCommonName(harvestersIndex) < - AIUtils.CountActorByCommonName(refineries); - if (harvCountTooLow) - { - var harvesterType = Info.HarvesterTypes.Random(world.LocalRandom); - if (unitBuilder.RequestedProductionCount(bot, harvesterType) == 0) - unitBuilder.RequestUnitProduction(bot, harvesterType); - } - } } // Returns true if FindNextResource was called. @@ -273,5 +438,25 @@ namespace OpenRA.Mods.Common.Traits if (resourceLayer != null) resourceLayer.CellChanged -= ResourceCellChanged; } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) + { + if (respondToAttackCooldown > 0 || !Info.HarvesterTypes.Contains(self.Info.Name) + || e.Attacker == null || e.Attacker.IsDead || !e.Attacker.AppearsHostileTo(self)) + return; + + var parach = self.TraitOrDefault(); + if (parach != null && parach.IsInAir) + return; + + var dockClientManager = self.Trait(); + if (dockClientManager.ReservedHostActor != null) + return; + + respondToAttackCooldown = 30; + var scanFromActor = dockClientManager.ClosestDock(null, forceEnter: true, ignoreOccupancy: true)?.Actor; + if (scanFromActor != null) + bot.QueueOrder(new Order("Dock", self, Target.FromActor(scanFromActor), false)); + } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/McvExpansionManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/McvExpansionManagerBotModule.cs new file mode 100644 index 0000000000..ff6ce9ee67 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/McvExpansionManagerBotModule.cs @@ -0,0 +1,824 @@ +#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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public enum BotMcvExpansionMode { CheckResource, CheckBase, CheckCurrentLocation } + + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI MCVs and expansion.")] + public class McvExpansionManagerBotModuleInfo : ConditionalTraitInfo, Requires, NotBefore + { + [Desc("Actor types that are considered MCVs (deploy into base builders).")] + public readonly HashSet McvTypes = []; + + [Desc("Actor types that are considered construction yards (base builders).")] + public readonly HashSet ConstructionYardTypes = []; + + [Desc("Actor types that are able to produce MCVs.")] + public readonly HashSet McvFactoryTypes = []; + + [Desc("Try to maintain at least this many ConstructionYardTypes, build an MCV if number is below this.")] + public readonly int MinimumConstructionYardCount = 1; + + [Desc("Try to maintain at additional this many ConstructionYardTypes.")] + public readonly int AdditionalConstructionYardCount = 0; + + [Desc("Build additional MCV if cash is above this.")] + public readonly int BuildAdditionalMCVCashAmount = 5000; + + [Desc("Delay (in ticks) for giving orders to idle MCVs.")] + public readonly int ScanForNewMcvInterval = 20; + + [Desc("Delay (in ticks) for checking and building a MCV.")] + public readonly int BuildMcvInterval = 101; + + [Desc("Delay (in ticks) for moving a conyard to better expansion. Only work with more than 1 conyard.")] + public readonly int MoveConyardTick = 4000; + + [Desc("Should moving the oldest or newest conyard be preferred? Random ordering if unset.")] + public readonly bool? MoveOldConyardFirst = null; + + [Desc("Initial expansion mode chosen by AI.")] + public readonly BotMcvExpansionMode InitialExpansionMode = BotMcvExpansionMode.CheckResource; + + [Desc("Allow the bot to switch expansion mode automatically on enough failure or successful attempts.")] + public readonly bool ExpansionModeAutoSwitch = true; + + /* those are CheckResource mode options */ + [Desc("Minimum distance (in cells) from the found resource creator location when checking for MCV deployment location.")] + public readonly int CRmodeMinDeployRadius = 2; + + [Desc("Maximum distance (in cells) the found resource creator location when checking for MCV deployment location.")] + public readonly int CRmodeMaxDeployRadius = 20; + + [Desc("When moving to a resource, what distance (in cells) to resource should we attempt to maintain?")] + public readonly int CRmodeTryMaintainRange = 8; + + [Desc("Distance (in cells) to avoid a friendly conyard when choosing an expansion location.", + "Recommended to set it equal or larger than ResourceMapStrideRadius.")] + public readonly int CRmodeFriendlyConyardDislikeRange = 14; + + [Desc("Distance (in cells) to avoid a friendly refinery when choosing an expansion location.", + "Recommended to set it equal or larger than ResourceMapStrideRadius.")] + public readonly int CRmodeFriendlyRefineryDislikeRange = 14; + + /* those are CheckBase mode options */ + [Desc("Minimum distance (in cells) from center of the base expansion when checking for MCV deployment location.")] + public readonly int CBmodeMinDeployRadius = 2; + + [Desc("Maximum distance (in cells) from center of the base expansion when checking for MCV deployment location.")] + public readonly int CBmodeMaxDeployRadius = 20; + + public override object Create(ActorInitializer init) { return new McvExpansionManagerBotModule(init.Self, this); } + } + + public class McvExpansionManagerBotModule : + ConditionalTrait, + IBotTick, + IBotRespondToAttack, + IBotBaseExpansion, + INotifyActorDisposing + { + // When ExpansionModeAutoSwitch is true, if the AI fails to find a deploy spot enough time even in CheckBase mode + // NegativeMaxFailedAttempts is applied to make AI switch bettween modes more frequently until a successful attempt + const int CRmodPositiveMaxFailedAttempts = 3; + const int CBmodPositiveMaxFailedAttempts = 2; + const int NegativeMaxFailedAttempts = 0; + + readonly World world; + readonly Player player; + readonly ActorIndex.OwnerAndNamesAndTrait mcvs; + readonly ActorIndex.OwnerAndNamesAndTrait constructionYards; + readonly ActorIndex.OwnerAndNamesAndTrait mcvFactories; + + IBotPositionsUpdated[] notifyPositionsUpdated; + IBotRequestUnitProduction[] requestUnitProduction; + IBotSuggestRefineryProduction[] suggestRefineryProduction; + + readonly Dictionary activeMCVs = []; + + PathFinder pathfinder; + ResourceMapBotModule resourceMapModule; + PlayerResources playerResources; + Actor mustUndeployCoyard; + + int scanInterval; + int buildMCVInterval; + int moveConyardInterval; + bool firstTick = true; + bool undeployEvenNoBase = false; + bool allowfallback = true; + + BotMcvExpansionMode mcvExpansionMode; + int mcvDeploymentMinDeployRadius; + int mcvDeploymentMaxDeployRadius; + int mcvDeploymentTryMaintainRange; + int maxFailedAttempts; + + int failedAttempts; + CPos? lastFailedCheckSpot; + + // It is unnecessary to respond every tick, we only need to respond once in a while. + int attackrespondcooldown = 20; + + int pathDistanceSquareFactor; + + public McvExpansionManagerBotModule(Actor self, McvExpansionManagerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + mcvs = new ActorIndex.OwnerAndNamesAndTrait(world, info.McvTypes, player); + constructionYards = new ActorIndex.OwnerAndNamesAndTrait(world, info.ConstructionYardTypes, player); + mcvFactories = new ActorIndex.OwnerAndNamesAndTrait(world, info.McvFactoryTypes, player); + } + + protected override void Created(Actor self) + { + // Special case handling is required for the Player actor. + // Created is called before Player.PlayerActor is assigned, + // so we must query player traits from self, which refers + // for bot modules always to the Player actor. + notifyPositionsUpdated = self.TraitsImplementing().ToArray(); + requestUnitProduction = self.TraitsImplementing().ToArray(); + suggestRefineryProduction = self.TraitsImplementing().ToArray(); + pathfinder = world.WorldActor.Trait(); + playerResources = self.Owner.PlayerActor.Trait(); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanInterval = world.LocalRandom.Next(Info.ScanForNewMcvInterval, Info.ScanForNewMcvInterval << 1); + buildMCVInterval = world.LocalRandom.Next(Info.BuildMcvInterval, Info.BuildMcvInterval << 1); + moveConyardInterval = world.LocalRandom.Next(Info.MoveConyardTick, Info.MoveConyardTick << 1); + } + + void SwitchExpansionMode(BotMcvExpansionMode nextMode) + { + mcvExpansionMode = nextMode; + switch (nextMode) + { + case BotMcvExpansionMode.CheckResource: + mcvDeploymentMinDeployRadius = Info.CRmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CRmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = Info.CRmodeTryMaintainRange; + break; + + case BotMcvExpansionMode.CheckBase: + mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = (Info.CBmodeMaxDeployRadius + Info.CBmodeMinDeployRadius) >> 1; + break; + + case BotMcvExpansionMode.CheckCurrentLocation: + mcvDeploymentMinDeployRadius = Info.CBmodeMinDeployRadius; + mcvDeploymentMaxDeployRadius = Info.CBmodeMaxDeployRadius; + mcvDeploymentTryMaintainRange = 0; + break; + + default: + break; + } + } + + void FindBadDeploySpot(CPos? failedSpot) + { + lastFailedCheckSpot = failedSpot; + + if (!Info.ExpansionModeAutoSwitch) + { + if (++failedAttempts >= maxFailedAttempts) + failedAttempts = maxFailedAttempts; + return; + } + + if (++failedAttempts >= maxFailedAttempts) + { + failedAttempts = 0; + switch (mcvExpansionMode) + { + case BotMcvExpansionMode.CheckResource: + SwitchExpansionMode(BotMcvExpansionMode.CheckBase); + break; + + case BotMcvExpansionMode.CheckBase: + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + maxFailedAttempts = NegativeMaxFailedAttempts; + break; + + case BotMcvExpansionMode.CheckCurrentLocation: + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + maxFailedAttempts = NegativeMaxFailedAttempts; + break; + } + } + } + + void FindGoodDeploySpot() + { + lastFailedCheckSpot = null; + + if (!Info.ExpansionModeAutoSwitch) + { + if (--failedAttempts <= -maxFailedAttempts) + failedAttempts = -maxFailedAttempts; + return; + } + + if (--failedAttempts <= -maxFailedAttempts) + { + switch (mcvExpansionMode) + { + case BotMcvExpansionMode.CheckResource: + maxFailedAttempts = CRmodPositiveMaxFailedAttempts; + failedAttempts = -maxFailedAttempts; + break; + + case BotMcvExpansionMode.CheckBase: + maxFailedAttempts = CRmodPositiveMaxFailedAttempts; + failedAttempts = maxFailedAttempts - 1; + SwitchExpansionMode(BotMcvExpansionMode.CheckResource); + break; + + case BotMcvExpansionMode.CheckCurrentLocation: + maxFailedAttempts = CBmodPositiveMaxFailedAttempts; + failedAttempts = maxFailedAttempts - 1; + SwitchExpansionMode(BotMcvExpansionMode.CheckBase); + break; + } + } + } + + public (CPos? ExpandLocation, int Attraction, CPos? CheckSpot) GetExpansionCenter(Actor mcv, Mobile mobile, bool allowfallback) + { + /* + * indiceSideLengthSquare (which is equal to indiceSideLength * indiceSideLength) is used as the basic unit to calculate the attraction of a candidate, + * we compare the attraction on the same scale on different factors, such as candidate's distance to current MCV and ally construction yard & refinery within range, etc: + * + * 1). the weight of candidate's distance-square to current MCV + * + * a) if not Mobile: range from 0 to -indiceSideLengthSquare. + * + * The reason why: + * + * It is calculated as "(candidate - mcv.Location).LengthSquared / pathDistanceSquareFactor". + * note that: pathDistanceSquareFactor = resourceMapIndicesColumnCount * resourceMapIndicesColumnCount + resourceMapIndicesRowCount * resourceMapIndicesRowCount, + * + * Consider a map, we divide it at the length of indiceSideLength = r, and then its resourceMapIndicesColumnCount = a, resourceMapIndicesRowCount = b, + * so the map.width ≈ a*r, map.height ≈ b*r, + * the maximum euclid distance-square between two points on the map is (a*r)(a*r) + (b*r)(b*r), + * so the maximum "weight of candidate's distance to current MCV" is from 0 to -((a*r)(a*r) + (b*r)(b*r)) / (a*a + b*b) = -r*r = -indiceSideLengthSquare. + * + * b) if Mobile: range depends on pathfinding distance in cell. + * + * It is calculated as "pathfindDistance * pathfindDistance / pathDistanceSquareFactor". + * + * 2). the weight of friendly construction yard within range: -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2. + * + * 3). the weight of enemy high threat within range: -indiceSideLengthSquare*8, otherwise -indiceSideLengthSquare/64 + * + * 4). the weight of friendly refinery within range (not for CheckBase mode): -indiceSideLengthSquare. If it belongs to an ally, -indiceSideLengthSquare/2. + * + * 5). the weight of resource amount (only for CheckResource mode): from 0 to +indiceSideLengthSquare/8. + * + * The reason why: + * + * The maximum resource amount in a indice of resource map is approximately indiceSideLengthSquare (full of it), but a stride full of resources is less likely to + * have room for buildings. So we prefer the indice have half of resource cells the most, which may give us enough room to place buildings. + * + * so the weight can be: (indiceSideLengthSquare/2) - |(indiceResourceCellCount - (indiceSideLengthSquare/2))|, range from (0 to +indiceSideLengthSquare/2). + * + * Note: In practive resource weight is not very important, we cannot let MCV go a long way just for a rich resource spot. + * We have to take only 1/4 of it, wich is (0 to +indiceSideLengthSquare/8), + * and apply some additional method to filter the indice for acceptable resource (not too low). + */ + var indiceSideLengthSquare = resourceMapModule.GetIndiceSideLength() * resourceMapModule.GetIndiceSideLength(); + switch (mcvExpansionMode) + { + /* + * CheckBase mode only considers the distance to current MCV, ally construction yard within range and enemy buildings within range. + * Attaction has a base value of indiceSideLengthSquare >> 3 (1/8 of the maximum distance weight, 1/(2*sqrt(2))≈ 1/2.8 of the maximum distance in map) + */ + case BotMcvExpansionMode.CheckBase: + var cb_conyardlocs = world.ActorsHavingTrait() + .Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name)) + .Select(a => (a.Location, a.Owner == player)) + .ToArray(); + CPos? cb_suitablespot = null; + CPos? cb_checkspot = null; + var cb_best = int.MinValue; + + for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++) + { + var indiceCenter = resourceMapModule.GetIndice(i).IndiceCenter; + + if (lastFailedCheckSpot == indiceCenter) + continue; + + var attraction = indiceSideLengthSquare >> 1; + + attraction -= (indiceCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor; + + attraction -= CalculateThreats(indiceSideLengthSquare, i); + + foreach (var (location, isAlly) in cb_conyardlocs) + { + var sdistance = (indiceCenter - location).LengthSquared; + if (sdistance <= indiceSideLengthSquare) + { + if (isAlly) + attraction -= indiceSideLengthSquare; + else + attraction -= indiceSideLengthSquare << 1; + } + } + + foreach (var (othermcv, dest) in activeMCVs) + { + if (dest == indiceCenter && othermcv != mcv) + attraction -= indiceSideLengthSquare << 1; + } + + if (!allowfallback) + { + var sdistance = (indiceCenter - mcv.Location).LengthSquared; + if (sdistance <= indiceSideLengthSquare) + attraction -= indiceSideLengthSquare << 1; + } + + if (attraction > cb_best) + { + cb_best = attraction; + cb_checkspot = indiceCenter; + cb_suitablespot = indiceCenter; + } + } + + return (cb_suitablespot ?? mcv.Location, cb_best, cb_checkspot); + + /* + * CheckResource mode considers the distance to current MCV, ally construction yard & refinery within range, + * Attaction has a base value of: + * 1. if not Mobile: indiceSideLengthSquare >> 4 (1/16 of the maximum distance weight, = 0.25 of the maximum euclid distance in map) + * 2. if Mobile: indiceSideLengthSquare >> 3 (1/8 of the maximum distance weight, ≈ 0.35 of the maximum euclid distance in map) + */ + case BotMcvExpansionMode.CheckResource: + + var cr_refinarylocs = world.ActorsHavingTrait() + .Where(a => a.Owner == player && resourceMapModule.Info.RefineryTypes.Contains(a.Info.Name)) + .Select(a => (a.Location, a.Owner != player)) + .ToArray(); + + var cr_conyardlocs = world.ActorsHavingTrait() + .Where(a => a.Owner.IsAlliedWith(player) && Info.ConstructionYardTypes.Contains(a.Info.Name)) + .Select(a => (a.Location, a.Owner != player)) + .ToArray(); + + // We only take indice has more than half of average indice value (in weight calculation), to skip the indice with very poor resource + // when failedAttempts is acceptable. + var thresholdRes = 0; + for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++) + { + var resourceCellCounts = resourceMapModule.GetIndice(i).ResourceCellsCount; + thresholdRes += (indiceSideLengthSquare >> 1) - Math.Abs(resourceCellCounts - (indiceSideLengthSquare >> 1)); + } + + thresholdRes = (thresholdRes / resourceMapModule.GetIndicesLength()) >> 1; + + CPos? cr_suitablespot = null; + CPos? cr_checkspot = null; + var cr_best = int.MinValue; + + for (var i = 0; i < resourceMapModule.GetIndicesLength(); i++) + { + var indice = resourceMapModule.GetIndice(i); + var indiceCenter = indice.IndiceCenter; + var resourceCellsCount = indice.ResourceCellsCount; + var resourceCellsCenter = indice.ResourceCellsCenter; + var resourceCreatorLocs = indice.ResourceCreatorLocs; + + if ((failedAttempts > maxFailedAttempts >> 1 && resourceCellsCount <= thresholdRes) || lastFailedCheckSpot == indiceCenter) + continue; + + var attraction = 0; + if (mobile == null) + { + attraction = indiceSideLengthSquare >> 2; + attraction -= (resourceCellsCenter - mcv.Location).LengthSquared / pathDistanceSquareFactor; + } + else + { + attraction = indiceSideLengthSquare >> 1; + + var path = pathfinder.FindPathToTargetCells(mcv, mcv.Location, [resourceCellsCenter], BlockedByActor.None); + + if (path == PathFinder.NoPath) + continue; + + attraction -= path.Count * path.Count / pathDistanceSquareFactor; + } + + // it is better that resource cells takes only half of the indice cells, which give us the place to place building. + attraction += ((indiceSideLengthSquare >> 1) - Math.Abs(resourceCellsCount - (indiceSideLengthSquare >> 1))) >> 2; + attraction += 8 * resourceCreatorLocs.Length; + + var resCenter = resourceCreatorLocs.Length == 0 || world.LocalRandom.Next(2) > 0 ? resourceCellsCenter : resourceCreatorLocs.Random(world.LocalRandom); + + attraction -= CalculateThreats(indiceSideLengthSquare, i); + + foreach (var (location, isAlly) in cr_refinarylocs) + { + var sdistance = (resCenter - location).LengthSquared; + if (sdistance <= Info.CRmodeFriendlyRefineryDislikeRange * Info.CRmodeFriendlyRefineryDislikeRange) + { + if (isAlly) + attraction -= indiceSideLengthSquare; + else + attraction -= indiceSideLengthSquare << 1; + } + } + + foreach (var (location, isAlly) in cr_conyardlocs) + { + var sdistance = (resCenter - location).LengthSquared; + if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange) + { + if (isAlly) + attraction -= indiceSideLengthSquare; + else + attraction -= indiceSideLengthSquare << 1; + } + } + + foreach (var (othermcv, dest) in activeMCVs) + { + if (dest == indiceCenter) + attraction -= indiceSideLengthSquare << 1; + } + + if (!allowfallback) + { + var sdistance = (resCenter - mcv.Location).LengthSquared; + if (sdistance <= Info.CRmodeFriendlyConyardDislikeRange * Info.CRmodeFriendlyConyardDislikeRange) + attraction -= indiceSideLengthSquare << 1; + } + + if (attraction > cr_best) + { + cr_best = attraction; + cr_checkspot = indiceCenter; + cr_suitablespot = resCenter; + } + } + + if (cr_suitablespot == null) + return (null, int.MinValue, null); + + return (cr_suitablespot, cr_best, cr_checkspot); + + case BotMcvExpansionMode.CheckCurrentLocation: + return (mcv.Location, int.MaxValue, null); + + default: + return (null, int.MinValue, null); + } + } + + int CalculateThreats(int indiceSideLengthSquare, int index) + { + var baseIndice = resourceMapModule.GetIndice(index); + + var (indiceCount, nearbyEnemyBaseThreat, nearbyEnemyThreat) = resourceMapModule.GetNearbyIndicesThreat(index); + + var indiceEnemyBaseThreat = Math.Max(baseIndice.EnemyBaseCount - baseIndice.FriendlyBaseCount, 0); + + var indiceEnemyUnitThreat = Math.Max(baseIndice.EnemyUnitCount - baseIndice.FriendlyUnitCount, 0); + + if (indiceCount == 0) + return (indiceEnemyUnitThreat * indiceSideLengthSquare >> 6) + (indiceEnemyBaseThreat * indiceSideLengthSquare << 3); + + return ((indiceEnemyUnitThreat + nearbyEnemyThreat / indiceCount) * indiceSideLengthSquare >> 6) + + ((indiceEnemyBaseThreat + nearbyEnemyBaseThreat / indiceCount) * indiceSideLengthSquare << 3); + } + + void IBotTick.BotTick(IBot bot) + { + attackrespondcooldown--; + + if (firstTick) + { + resourceMapModule = bot.Player.PlayerActor.TraitsImplementing().First(t => t.IsTraitEnabled()); + SwitchExpansionMode(Info.InitialExpansionMode); + + pathDistanceSquareFactor = resourceMapModule.GetIndiceRowCount() * resourceMapModule.GetIndiceRowCount() + + resourceMapModule.GetIndiceColumnCount() * resourceMapModule.GetIndiceColumnCount(); + + DeployMcvs(bot, false); + firstTick = false; + } + + if (--scanInterval <= 0) + { + foreach (var amcv in activeMCVs.Keys.ToList()) + { + if (amcv.IsDead || !amcv.IsInWorld) + activeMCVs.Remove(amcv); + } + + scanInterval = Info.ScanForNewMcvInterval; + DeployMcvs(bot, true); + } + + if (--buildMCVInterval <= 0) + { + buildMCVInterval = Info.BuildMcvInterval; + BuildMCV(bot); + } + + if (--moveConyardInterval <= 0) + { + foreach (var amcv in activeMCVs.Keys.ToList()) + { + if (amcv.IsDead || !amcv.IsInWorld) + activeMCVs.Remove(amcv); + } + + moveConyardInterval = Info.MoveConyardTick; + UnDeployConyard(bot); + } + } + + void BuildMCV(IBot bot) + { + if (Info.McvTypes.Count <= 0) + return; + if (AIUtils.CountActorByCommonName(mcvFactories) <= 0) + return; + var mcvNum = AIUtils.CountActorByCommonName(mcvs); + var conyardNum = AIUtils.CountActorByCommonName(constructionYards); + + var mcvShouldHave = playerResources.GetCashAndResources() >= Info.BuildAdditionalMCVCashAmount + ? Info.MinimumConstructionYardCount + Info.AdditionalConstructionYardCount : Info.MinimumConstructionYardCount; + + // If we only have 1 MCV and no conyard, we should be allowed to build another MCV. + // Otherwise, when an mcv is on the move and we should wait. + if ((conyardNum <= 0 && mcvNum > 1) || (conyardNum > 0 && mcvNum > 0)) + return; + + if (conyardNum + mcvNum >= mcvShouldHave) + return; + + // We have MCV in production queue, let's wait. + if (mcvFactories.Actors + .Any(a => !a.IsDead && a.TraitsImplementing().Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item))))) + return; + + // We have MCV in production queue, let's wait. + if (player.PlayerActor.TraitsImplementing() + .Any(t => t.Enabled && t.AllQueued().Any(q => Info.McvTypes.Contains(q.Item)))) + return; + var unitBuilder = requestUnitProduction.FirstEnabledTraitOrDefault(); + if (unitBuilder == null) + return; + var mcvType = Info.McvTypes.Random(world.LocalRandom); + + // Make sure we only request one MCV at a time. + if (unitBuilder.RequestedProductionCount(bot, mcvType) <= 0) + unitBuilder.RequestUnitProduction(bot, mcvType); + } + + void DeployMcvs(IBot bot, bool chooseLocation) + { + var newMCVs = world.ActorsHavingTrait() + .Where(a => a.Owner == player && a.IsIdle && Info.McvTypes.Contains(a.Info.Name)); + + foreach (var mcv in newMCVs) + DeployMcv(bot, mcv, chooseLocation); + } + + void UnDeployConyard(IBot bot) + { + if (mustUndeployCoyard != null && mustUndeployCoyard.IsInWorld && !mustUndeployCoyard.IsDead && mustUndeployCoyard.Owner == player) + { + bot.QueueOrder(new Order("DeployTransform", mustUndeployCoyard, true)); + mustUndeployCoyard = null; + + return; + } + + if (activeMCVs.Count > 0) + return; + + var conyards = constructionYards.Actors + .Where(a => !a.IsDead); + + var moveOldConyardFirst = Info.MoveOldConyardFirst ?? world.LocalRandom.Next(2) > 0; + + if (moveOldConyardFirst) + conyards = conyards.OrderBy(a => a.ActorID); + else + conyards = conyards.OrderByDescending(a => a.ActorID); + + var conyardslist = conyards.ToList(); + + if (conyardslist.Count > 1 || undeployEvenNoBase) + { + // We don't want to interrupt refinery production, otherwise it may cause a dead loop of deploy/undeploy. + var movableMCV = conyardslist.FirstOrDefault(a => !a.TraitsImplementing() + .Any(t => t.Enabled && t.AllQueued().Any(q => resourceMapModule.Info.RefineryTypes.Contains(q.Item)))); + + if (movableMCV != null) + bot.QueueOrder(new Order("DeployTransform", movableMCV, true)); + + undeployEvenNoBase = false; + } + } + + // Find any MCV and deploy them at a sensible location. + void DeployMcv(IBot bot, Actor mcv, bool move) + { + CPos? desiredLocation = null; + var transformsInfo = mcv.Info.TraitInfo(); + var actorInfo = world.Map.Rules.Actors[transformsInfo.IntoActor]; + var bi = actorInfo.TraitInfoOrDefault(); + if (bi == null) + return; + + if (move) + { + var (deployLocation, resLoc, checkloc) = ChooseMcvDeployLocation(mcv, actorInfo, bi, transformsInfo.Offset, allowfallback); + allowfallback = true; + desiredLocation = deployLocation; + if (desiredLocation == null) + return; + + activeMCVs[mcv] = checkloc.Value; + if (resLoc != null) + { + foreach (var srp in suggestRefineryProduction) + srp.RequestLocation(resLoc.Value, desiredLocation.Value, mcv); + } + + bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true)); + } + else + { + if (!world.CanPlaceBuilding(mcv.Location + transformsInfo.Offset, actorInfo, bi, mcv)) + return; + desiredLocation = mcv.Location; + } + + bot.QueueOrder(new Order("DeployTransform", mcv, true)); + + // When we don't have a construction yard, we notify the new location to other traits for defence, + // If not, we only notify sometimes, because we are not sure if mcv can successfully deploy at the desired location. + // TODO: This could be addressed via INotifyTransform. + if (constructionYards.Actors.All(a => a.IsDead) || world.LocalRandom.Next(2) > 0) + { + foreach (var n in notifyPositionsUpdated) + { + n.UpdatedBaseCenter(desiredLocation.Value); + n.UpdatedDefenseCenter(desiredLocation.Value); + } + } + } + + // First, find a suitable expansion location according to current mode, + // Then, find a deployable cell around it. + (CPos? DeployLoc, CPos? ResourceLoc, CPos? CheckLoc) ChooseMcvDeployLocation( + Actor mcv, + ActorInfo transformIntoInfo, + BuildingInfo transformIntoBuildingInfo, + CVec offset, + bool allowfallback) + { + if (!mcv.Info.HasTraitInfo()) + return (null, null, null); + + var mobile = mcv.TraitOrDefault(); + + var (expandCenter, attraction, checkspot) = GetExpansionCenter(mcv, mobile, allowfallback); + + // Find the deployable cell + CPos? FindDeployCell(CPos? sourceCell, CPos? targetCell, int minRange, int maxRange, int tryMaintainRange) + { + if (!sourceCell.HasValue || !targetCell.HasValue) + return null; + + var target = targetCell.Value; + var source = sourceCell.Value; + + var cells = world.Map.FindTilesInAnnulus(target, minRange, maxRange); + + /* First, sort the cells that keep tryMaintainRange to target (meanwhile direction is from center to target) the first to be considered + * by using following code. The idea is to use a linear combination of two distances-square for sorting weight. + * + * See comments in https://github.com/OpenRA/OpenRA/pull/22028#issuecomment-3242518793 for explaination. + */ + if (source != target) + { + var theta = tryMaintainRange; + var deta = (target - source).Length - tryMaintainRange; + cells = cells.OrderBy(c => deta * (c - target).LengthSquared + theta * (c - source).LengthSquared); + } + else + cells = cells.Shuffle(world.LocalRandom); + + CPos? bestcell = null; + foreach (var cell in cells) + { + if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv)) + { + bestcell = cell; + break; + } + } + + // If no deployble cell found, return null + if (bestcell == null) + return null; + + if (source != target && mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, bestcell.Value)) + bestcell = null; + + // If the best deploy cell is not ideal ( >= tryMaintainRange + 2), which means there might be some huge blockers + // so we fall back to default behavior, which is the directly closest cell to target + if (!bestcell.HasValue || (source != target && (bestcell.Value - target).LengthSquared >= (tryMaintainRange + 2) * (tryMaintainRange + 2))) + { + cells = cells.OrderBy(c => (c - target).LengthSquared); + foreach (var cell in cells) + { + if (world.CanPlaceBuilding(cell + offset, transformIntoInfo, transformIntoBuildingInfo, mcv)) + { + if (mobile != null && !pathfinder.PathMightExistForLocomotorBlockedByImmovable(mobile.Locomotor, source, cell)) + return null; + + return (!bestcell.HasValue) || (cell - target).LengthSquared < (bestcell.Value - target).LengthSquared ? cell : bestcell; + } + } + } + + return bestcell; + } + + var bc = FindDeployCell(mcv.Location, expandCenter, mcvDeploymentMinDeployRadius, mcvDeploymentMaxDeployRadius, mcvDeploymentTryMaintainRange); + + // At last, if the attraction of the found expansion location is good enough (>0) and deploy cell found, + // we consider it as a good expansion, otherwise, we consider it as a bad expansion. + if (bc.HasValue && attraction > 0) + FindGoodDeploySpot(); + else + FindBadDeploySpot(bc.HasValue ? null : checkspot); + + if (mcvExpansionMode == BotMcvExpansionMode.CheckResource && expandCenter.HasValue && bc.HasValue) + return (bc, expandCenter, checkspot); + + return (bc, null, checkspot); + } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) + { + if (attackrespondcooldown <= 0 && Info.McvTypes.Contains(self.Info.Name)) + { + attackrespondcooldown = 20; + + DeployMcv(bot, self, false); + + if (AIUtils.CountActorByCommonName(constructionYards) == 0) + { + foreach (var n in notifyPositionsUpdated) + n.UpdatedBaseCenter(self.Location); + } + } + } + + void INotifyActorDisposing.Disposing(Actor self) + { + mcvs.Dispose(); + constructionYards.Dispose(); + mcvFactories.Dispose(); + } + + void IBotBaseExpansion.UpdateExpansionParams(IBot bot, bool fallback, bool undeployEvenNoBase, Actor mustUndeploy) + { + moveConyardInterval = 20; // allow some order latency + allowfallback = fallback; + this.undeployEvenNoBase = undeployEvenNoBase; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/ResourceMapBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/ResourceMapBotModule.cs new file mode 100644 index 0000000000..fe486525d6 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/ResourceMapBotModule.cs @@ -0,0 +1,308 @@ +#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; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.Player)] + public class ResourceMapBotModuleInfo : ConditionalTraitInfo, NotBefore + { + [Desc("Harvestable and valuable resource types.")] + public readonly HashSet ValuableResourceTypes = []; + + [Desc("Tells the AI what types are considered resource creator.")] + public readonly HashSet ResourceCreatorTypes = []; + + [Desc($"Actor types that are considered refineries for {nameof(HarvesterTypes)}.")] + public readonly HashSet RefineryTypes = []; + + [Desc($"Actor types that are considered harvesters for {nameof(ValuableResourceTypes)}.")] + public readonly HashSet HarvesterTypes = []; + + [Desc("Actor types that are considered to be the base building for expansion. Other enemy units will also be recorded", + "Defence and production building is suggested")] + public readonly HashSet EnemyBaseBuildingTypes = []; + + [Desc("Delay (in ticks) for updating the indicies.")] + public readonly int UpdateResourceMapInverval = 67; + + [Desc("The size (in cells) of half of the side length of the indice (in square).")] + public readonly int ResourceMapStrideRadius = 12; + + public override object Create(ActorInitializer init) { return new ResourceMapBotModule(init.Self, this); } + } + + public class ResourceIndice + { + public int2 IndiceIndex; + public CPos IndiceCenter; + public int ResourceCellsCount; + public CPos ResourceCellsCenter; + public CPos[] ResourceCreatorLocs; + public int PlayerRefineryCount; + public int PlayerHarvetserCount; + + public int EnemyUnitCount; + public int EnemyBaseCount; + public int FriendlyBaseCount; + public int FriendlyUnitCount; + + public ResourceIndice(int2 indiceIndex, CPos indiceCenter) + { + IndiceIndex = indiceIndex; + IndiceCenter = indiceCenter; + ResourceCreatorLocs = []; + } + } + + public class ResourceMapBotModule : IBotTick + { + readonly World world; + readonly Player player; + IResourceLayer resourceLayer; + public readonly ResourceMapBotModuleInfo Info; + + ResourceIndice[] resourceMapIndices = null; + readonly int indiceSideLength; + readonly int indiceResourceScanRadius; + int resourceMapIndicesColumnCount; + int resourceMapIndicesRowCount; + + int updateResourceMapIndex = 0; + int updateResourceMapInterval; + bool firstTick = true; + + public ResourceMapBotModule(Actor self, ResourceMapBotModuleInfo info) + { + world = self.World; + player = self.Owner; + indiceSideLength = info.ResourceMapStrideRadius << 1; + Info = info; + + // FindTilesInAnnulus returns cells in a rough circle shape, and resourceMapIndices are divided in square, + // so we need a larger range to cover cells in the indice approximately, but avoid takes too much other indices' cells. + indiceResourceScanRadius = info.ResourceMapStrideRadius * 12 / 10; // ≈ * (sqrt(2) + 1) / 2 + } + + void IBotTick.BotTick(IBot bot) + { + if (firstTick) + { + resourceLayer = world.WorldActor.TraitOrDefault(); + updateResourceMapInterval = world.LocalRandom.Next(Info.UpdateResourceMapInverval, Info.UpdateResourceMapInverval << 1); + + if (resourceMapIndices == null && resourceLayer != null) + { + var map = world.Map; + var actualMapWidth = map.Bounds.Width; + var actualMapHeight = map.Bounds.Height; + var xoffset = map.Bounds.X; + var yoffset = map.Bounds.Y; + + resourceMapIndicesColumnCount = (actualMapWidth + indiceSideLength - 1) / indiceSideLength; + resourceMapIndicesRowCount = (actualMapHeight + indiceSideLength - 1) / indiceSideLength; + + var overallIndiceWidth = resourceMapIndicesColumnCount * indiceSideLength; + var overallIndiceHeight = resourceMapIndicesRowCount * indiceSideLength; + + xoffset -= (overallIndiceWidth - actualMapWidth) >> 1; + yoffset -= (overallIndiceHeight - actualMapHeight) >> 1; + + resourceMapIndices = Exts.MakeArray(resourceMapIndicesColumnCount * resourceMapIndicesRowCount, + i => new ResourceIndice(new int2(i % resourceMapIndicesColumnCount, i / resourceMapIndicesColumnCount), + new MPos( + xoffset + i % resourceMapIndicesColumnCount * indiceSideLength + (indiceSideLength >> 1), + yoffset + i / resourceMapIndicesColumnCount * indiceSideLength + (indiceSideLength >> 1)).ToCPos(map))); + + for (var i = 0; i < resourceMapIndices.Length; i++) + UpdateResourceMap(i); + } + + firstTick = false; + } + + if (--updateResourceMapInterval <= 0) + { + updateResourceMapInterval = Info.UpdateResourceMapInverval; + UpdateResourceMap(updateResourceMapIndex); + updateResourceMapIndex = (updateResourceMapIndex + 1) % resourceMapIndices.Length; + } + } + + void UpdateResourceMap(int index) + { + if (resourceLayer == null || resourceMapIndices == null || resourceMapIndices.Length == 0) + return; + + var indice = resourceMapIndices[index]; + var sumCellsX = 0; + var sumCellsY = 0; + + var resTiles = world.Map.FindTilesInAnnulus(indice.IndiceCenter, 0, indiceResourceScanRadius).Where(c => + { + if (!Info.ValuableResourceTypes.Contains(resourceLayer.GetResource(c).Type)) + return false; + + sumCellsX += c.X; + sumCellsY += c.Y; + return true; + }).ToList(); + + var resTilesCount = resTiles.Count; + var bestCell = CPos.Zero; + if (resTilesCount != 0) + { + var resAvgCell = new CPos(sumCellsX / resTilesCount, sumCellsY / resTilesCount); + bestCell = resTiles[0]; + var bestDist = (bestCell - resAvgCell).LengthSquared; + foreach (var c in resTiles) + { + var dist = (c - resAvgCell).LengthSquared; + if (dist < bestDist) + { + bestDist = dist; + bestCell = c; + } + } + } + + var refineryCount = 0; + var harvesterCount = 0; + var normalEnemyCount = 0; + var highThreatEnemyCount = 0; + + var resourceCreatorLocs = world.FindActorsInCircle(world.Map.CenterOfCell(indice.IndiceCenter), WDist.FromCells(indiceResourceScanRadius)) + .Where(a => + { + if (a.Owner.RelationshipWith(player) == PlayerRelationship.Enemy) + { + if (Info.EnemyBaseBuildingTypes.Contains(a.Info.Name)) + highThreatEnemyCount++; + else + normalEnemyCount++; + } + else if (a.Owner.RelationshipWith(player) == PlayerRelationship.Ally) + { + if (Info.EnemyBaseBuildingTypes.Contains(a.Info.Name)) + indice.FriendlyBaseCount++; + else + indice.FriendlyUnitCount++; + + if (a.Owner == player) + { + if (Info.RefineryTypes.Contains(a.Info.Name)) + refineryCount++; + + if (Info.HarvesterTypes.Contains(a.Info.Name)) + harvesterCount++; + } + } + + return Info.ResourceCreatorTypes.Contains(a.Info.Name); + }).Select(a => a.Location).ToArray(); + + indice.ResourceCellsCount = resTilesCount; + indice.ResourceCellsCenter = bestCell; + indice.ResourceCreatorLocs = resourceCreatorLocs; + indice.PlayerRefineryCount = refineryCount; + indice.PlayerHarvetserCount = harvesterCount; + indice.EnemyUnitCount = normalEnemyCount; + indice.EnemyBaseCount = highThreatEnemyCount; + } + + public int GetIndicesLength() + { + return resourceMapIndices?.Length ?? 0; + } + + public int GetIndiceSideLength() + { + return indiceSideLength; + } + + public int GetIndiceColumnCount() + { + return resourceMapIndicesColumnCount; + } + + public int GetIndiceRowCount() + { + return resourceMapIndicesRowCount; + } + + public int GetIndiceScanRadius() + { + return indiceResourceScanRadius; + } + + public (int IndiceCount, int EnemyUnitCount, int EnemyBaseCount) GetNearbyIndicesThreat(int index) + { + var baseIndice = resourceMapIndices[index]; + + var indiceCount = 0; + var nearbyEnemyBase = 0; + var nearbyEnemyUnit = 0; + + var x = baseIndice.IndiceIndex.X; + var y = baseIndice.IndiceIndex.Y; + + var offsets = new int[] { -1, 0, 1 }; + + for (var i = 0; i < offsets.Length; i++) + { + for (var j = 0; j < offsets.Length; j++) + { + var offsetIndex = x + offsets[i] + (y + offsets[j]) * resourceMapIndicesColumnCount; + if (offsetIndex != index && offsetIndex >= 0 && offsetIndex < resourceMapIndices.Length) + { + var indice = resourceMapIndices[offsetIndex]; + nearbyEnemyBase += indice.EnemyBaseCount - indice.FriendlyBaseCount; + nearbyEnemyUnit += indice.EnemyUnitCount - indice.FriendlyUnitCount; + indiceCount++; + } + } + } + + return (indiceCount, Math.Max(nearbyEnemyUnit, 0), Math.Max(nearbyEnemyBase, 0)); + } + + public ResourceIndice GetIndice(int i) + { + if (resourceMapIndices == null || i >= resourceMapIndices.Length) + return null; + + return resourceMapIndices[i]; + } + + public ResourceIndice FindClosestIndiceFromCPos(CPos cpos) + { + var maxDist = int.MaxValue; + var best = 0; + + for (var i = 0; i < resourceMapIndices.Length; i++) + { + var index = resourceMapIndices[i]; + var dist = (index.IndiceCenter - cpos).LengthSquared; + if (dist < maxDist) + { + maxDist = dist; + best = i; + } + } + + return GetIndice(best); + } + } +} diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 7770791256..005b0aed01 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -634,6 +634,18 @@ namespace OpenRA.Mods.Common.Traits bool PauseUnitProduction { get; } } + [RequireExplicitImplementation] + public interface IBotBaseExpansion + { + void UpdateExpansionParams(IBot bot, bool fallback, bool undeployEvenNoBase, Actor mustUndeploy); + } + + [RequireExplicitImplementation] + public interface IBotSuggestRefineryProduction + { + void RequestLocation(CPos refineryLocation, CPos conyardLocation, Actor expandActor); + } + [RequireExplicitImplementation] public interface IEditorActorOptions : ITraitInfoInterface { diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20250330/RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20250330/RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule.cs new file mode 100644 index 0000000000..82e0d63b78 --- /dev/null +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20250330/RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule.cs @@ -0,0 +1,32 @@ +#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; + +namespace OpenRA.Mods.Common.UpdateRules.Rules +{ + sealed class RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule : UpdateRule + { + public override string Name => "Remove BarracksTypes and VehiclesTypes in BaseBuilderBotModule"; + public override string Description => "BarracksTypes and VehiclesTypes were removed and now BaseBuilderBotModule check by using ProductionTypes."; + + public override IEnumerable UpdateActorNode(ModData modData, MiniYamlNodeBuilder actorNode) + { + foreach (var baseBuilder in actorNode.ChildrenMatching("BaseBuilderBotModule")) + { + baseBuilder.RemoveNodes("BarracksTypes"); + baseBuilder.RemoveNodes("VehiclesFactoryTypes"); + } + + yield break; + } + } +} diff --git a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs index 76a1b3a3ca..93dc373728 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdatePath.cs @@ -72,6 +72,7 @@ namespace OpenRA.Mods.Common.UpdateRules new ReplaceBaseAttackNotifier(), new RemoveBuildingInfoAllowPlacementOnResources(), new EditorMarkerTileLabels(), + new RemoveBarracksTypesAndVehiclesTypesInBaseBuilderBotModule(), ]), ]; diff --git a/mods/cnc/rules/ai.yaml b/mods/cnc/rules/ai.yaml index 7d658def3c..169af04d5f 100644 --- a/mods/cnc/rules/ai.yaml +++ b/mods/cnc/rules/ai.yaml @@ -82,68 +82,89 @@ Player: RequiresCondition: enable-cabal-ai || enable-watson-ai || enable-hal9001-ai HarvesterTypes: harv RefineryTypes: proc + InitialHarvesters: 3 + ResourceMapBotModule: + RequiresCondition: enable-hal9001-ai || enable-cabal-ai || enable-watson-ai + ResourceCreatorTypes: split2, split3, splitblue + ValuableResourceTypes: Tiberium, BlueTiberium + HarvesterTypes: harv + RefineryTypes: proc + EnemyBaseBuildingTypes: fact,pyle,hand,weap,afld,gtwr,gun,atwr,obli,hpad,nuke,nuk2,proc BaseBuilderBotModule@cabal: RequiresCondition: enable-cabal-ai BuildingQueues: Building.Nod, Building.GDI DefenseQueues: Support.Nod, Support.GDI - MinimumExcessPower: 30 - MaximumExcessPower: 150 - ExcessPowerIncrement: 30 - ExcessPowerIncreaseThreshold: 5 + MinimumExcessPower: 0 + MaximumExcessPower: 270 + ExcessPowerIncrement: 40 + ExcessPowerIncreaseThreshold: 3 + MaxBaseRadius: 18 ConstructionYardTypes: fact + ExpansionTolerate: 0 + ForceExpansionTolerate: 1 RefineryTypes: proc PowerTypes: nuke, nuk2 - BarracksTypes: pyle, hand - VehiclesFactoryTypes: weap, afld - ProductionTypes: pyle, hand, weap, afld, hpad + TechTypes: hq, tmpl, fix, eye, hpad + ProductionTypes: pyle, hand, weap, afld + NewProductionCashThreshold: 8000 SiloTypes: silo DefenseTypes: gtwr,gun,atwr,obli,sam BuildingLimits: - proc: 4 pyle: 3 hand: 3 hq: 1 weap: 3 afld: 3 - hpad: 0 + hpad: 1 eye: 1 tmpl: 1 fix: 1 silo: 1 BuildingFractions: - proc: 20 + nuke: 1 + proc: 1 pyle: 5 hand: 5 hq: 4 weap: 9 afld: 9 - gtwr: 5 - gun: 5 - atwr: 9 - obli: 7 + gtwr: 6 + gun: 6 + atwr: 20 + obli: 20 sam: 7 eye: 1 tmpl: 1 fix: 1 hpad: 2 + BuildingDelays: + hq: 4000 + fix: 4000 + hpad: 4100 + gun: 2000 + gtwr: 2000 + sam: 3000 BaseBuilderBotModule@watson: RequiresCondition: enable-watson-ai BuildingQueues: Building.Nod, Building.GDI DefenseQueues: Support.Nod, Support.GDI - MinimumExcessPower: 30 + MinimumExcessPower: 0 MaximumExcessPower: 150 - ExcessPowerIncrement: 30 - ExcessPowerIncreaseThreshold: 4 + ExcessPowerIncrement: 40 + ExcessPowerIncreaseThreshold: 3 + MaxBaseRadius: 18 ConstructionYardTypes: fact RefineryTypes: proc PowerTypes: nuke,nuk2 - BarracksTypes: pyle,hand - VehiclesFactoryTypes: weap,afld - ProductionTypes: pyle,hand,weap,afld,hpad + AdditionalMinimumRefineryCount: 0 + TechTypes: hq, tmpl, fix, eye, hpad + ProductionTypes: pyle,hand,weap,afld + ExpansionTolerate: 0 + ForceExpansionTolerate: 0 + NewProductionCashThreshold: 6000 SiloTypes: silo DefenseTypes: gtwr,gun,atwr,obli,sam BuildingLimits: - proc: 4 pyle: 3 hand: 3 hq: 1 @@ -155,7 +176,8 @@ Player: fix: 1 silo: 1 BuildingFractions: - proc: 17 + nuke: 1 + proc: 1 pyle: 2 hand: 2 hq: 1 @@ -170,24 +192,35 @@ Player: tmpl: 1 sam: 7 fix: 1 + BuildingDelays: + hq: 5000 + fix: 5000 + hpad: 6000 + gun: 2000 + gtwr: 2000 + sam: 3000 BaseBuilderBotModule@hal9001: RequiresCondition: enable-hal9001-ai BuildingQueues: Building.Nod, Building.GDI DefenseQueues: Support.Nod, Support.GDI - MinimumExcessPower: 30 - MaximumExcessPower: 210 - ExcessPowerIncrement: 30 - ExcessPowerIncreaseThreshold: 4 + MinimumExcessPower: 0 + MaximumExcessPower: 200 + ExcessPowerIncrement: 40 + ExcessPowerIncreaseThreshold: 2 ConstructionYardTypes: fact + MaxBaseRadius: 18 RefineryTypes: proc + InititalMinimumRefineryCount: 0 + AdditionalMinimumRefineryCount: 2 PowerTypes: nuke,nuk2 - BarracksTypes: pyle,hand - VehiclesFactoryTypes: weap,afld - ProductionTypes: pyle,hand,weap,afld,hpad + TechTypes: hq, tmpl, fix, eye, hpad + ProductionTypes: pyle,hand,weap,afld + ExpansionTolerate: 0 + ForceExpansionTolerate: 0 + NewProductionCashThreshold: 7000 SiloTypes: silo DefenseTypes: gtwr,gun,atwr,obli,sam BuildingLimits: - proc: 4 pyle: 4 hand: 4 hq: 1 @@ -199,7 +232,8 @@ Player: fix: 1 silo: 1 BuildingFractions: - proc: 17 + nuke: 1 + proc: 1 pyle: 7 hand: 9 hq: 1 @@ -214,18 +248,30 @@ Player: tmpl: 1 sam: 7 fix: 1 + BuildingDelays: + hq: 4000 + fix: 4000 + hpad: 4100 + gun: 2000 + gtwr: 2000 + sam: 3000 BuildingRepairBotModule: RequiresCondition: enable-cabal-ai || enable-watson-ai || enable-hal9001-ai SquadManagerBotModule@cabal: RequiresCondition: enable-cabal-ai - SquadSize: 15 - ExcludeFromSquadsTypes: harv, mcv, a10 + SquadSize: 50 + ExcludeFromSquadsTypes: harv, mcv, a10, mlrs ConstructionYardTypes: fact AirUnitsTypes: heli, orca ProtectionTypes: fact, fact.gdi, fact.nod, nuke, nuk2, proc, silo, pyle, hand, afld, weap, hpad, hq, fix, eye, tmpl, gun, sam, obli, gtwr, atwr, mcv, harv, miss IgnoredEnemyTargetTypes: Air + RushInterval: 1000 + MinimumAttackForceDelay: 1000 + ProtectUnitScanRadius: 30 + ProtectionScanRadius: 12 UnitBuilderBotModule@cabal: RequiresCondition: enable-cabal-ai + ProductionMinCashRequirement: 750 UnitQueues: Vehicle.Nod, Vehicle.GDI, Infantry.Nod, Infantry.GDI, Aircraft.Nod, Aircraft.GDI UnitsToBuild: e1: 65 @@ -238,6 +284,7 @@ Player: bike: 40 ltnk: 25 ftnk: 10 + apc: 10 arty: 60 stnk: 40 jeep: 5 @@ -246,17 +293,27 @@ Player: htnk: 50 heli: 5 orca: 5 + mlrs: 1 UnitLimits: harv: 8 - McvManagerBotModule: - RequiresCondition: enable-cabal-ai || enable-watson-ai || enable-hal9001-ai + mlrs: 3 + McvExpansionManagerBotModule@cabal: + RequiresCondition: enable-cabal-ai McvTypes: mcv ConstructionYardTypes: fact McvFactoryTypes: weap,afld + MinimumConstructionYardCount: 1 + AdditionalConstructionYardCount: 1 + McvExpansionManagerBotModule@watson-hal9001: + RequiresCondition: enable-watson-ai || enable-hal9001-ai + McvTypes: mcv + ConstructionYardTypes: fact + McvFactoryTypes: weap,afld + MinimumConstructionYardCount: 1 SquadManagerBotModule@watson: RequiresCondition: enable-watson-ai SquadSize: 15 - ExcludeFromSquadsTypes: harv, mcv, a10 + ExcludeFromSquadsTypes: harv, mcv, a10, mlrs ConstructionYardTypes: fact AirUnitsTypes: heli, orca ProtectionTypes: fact, fact.gdi, fact.nod, nuke, nuk2, proc, silo, pyle, hand, afld, weap, hpad, hq, fix, eye, tmpl, gun, sam, obli, gtwr, atwr, mcv, harv, miss @@ -275,24 +332,28 @@ Player: ftnk: 10 arty: 40 bike: 10 - heli: 10 + heli: 1 ltnk: 40 stnk: 40 - orca: 10 - msam: 50 + orca: 1 + msam: 40 htnk: 50 jeep: 20 mtnk: 50 + mlrs: 1 UnitLimits: harv: 8 + mlrs: 2 SquadManagerBotModule@hal9001: RequiresCondition: enable-hal9001-ai - SquadSize: 8 - ExcludeFromSquadsTypes: harv, mcv, a10 + SquadSize: 15 + ExcludeFromSquadsTypes: harv, mcv, a10, mlrs ConstructionYardTypes: fact AirUnitsTypes: heli, orca ProtectionTypes: fact, fact.gdi, fact.nod, nuke, nuk2, proc, silo, pyle, hand, afld, weap, hpad, hq, fix, eye, tmpl, gun, sam, obli, gtwr, atwr, mcv, harv, miss IgnoredEnemyTargetTypes: Air + ProtectUnitScanRadius: 15 + ProtectionScanRadius: 12 UnitBuilderBotModule@hal9001: RequiresCondition: enable-hal9001-ai UnitQueues: Vehicle.Nod, Vehicle.GDI, Infantry.Nod, Infantry.GDI, Aircraft.Nod, Aircraft.GDI @@ -319,3 +380,4 @@ Player: orca: 10 UnitLimits: harv: 8 + mlrs: 3 diff --git a/mods/d2k/rules/ai.yaml b/mods/d2k/rules/ai.yaml index 82a7fb290a..7123924f24 100644 --- a/mods/d2k/rules/ai.yaml +++ b/mods/d2k/rules/ai.yaml @@ -78,10 +78,10 @@ Player: ConstructionYardTypes: construction_yard RefineryTypes: refinery PowerTypes: wind_trap - VehiclesFactoryTypes: light_factory, heavy_factory, starport ProductionTypes: light_factory, heavy_factory, barracks, starport SiloTypes: silo DefenseTypes: medium_gun_turret,large_gun_turret + SellRefineryInterval: -1 BuildingLimits: barracks: 3 refinery: 4 @@ -137,10 +137,10 @@ Player: ConstructionYardTypes: construction_yard RefineryTypes: refinery PowerTypes: wind_trap - VehiclesFactoryTypes: light_factory, heavy_factory, starport ProductionTypes: light_factory, heavy_factory, barracks, starport SiloTypes: silo DefenseTypes: medium_gun_turret,large_gun_turret + SellRefineryInterval: -1 BuildingLimits: barracks: 3 refinery: 4 @@ -194,10 +194,10 @@ Player: ConstructionYardTypes: construction_yard RefineryTypes: refinery PowerTypes: wind_trap - VehiclesFactoryTypes: light_factory, heavy_factory, starport ProductionTypes: light_factory, heavy_factory, barracks, starport SiloTypes: silo DefenseTypes: medium_gun_turret,large_gun_turret + SellRefineryInterval: -1 BuildingLimits: barracks: 3 refinery: 4 diff --git a/mods/ra/maps/soviet-05/rules.yaml b/mods/ra/maps/soviet-05/rules.yaml index 60831910e7..66551e6ec7 100644 --- a/mods/ra/maps/soviet-05/rules.yaml +++ b/mods/ra/maps/soviet-05/rules.yaml @@ -18,8 +18,6 @@ Player: ConstructionYardTypes: fact RefineryTypes: proc PowerTypes: powr - BarracksTypes: tent - VehiclesFactoryTypes: weap ProductionTypes: tent, weap SiloTypes: silo BuildingLimits: diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index 2a9bfe2f15..cb32aafb91 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -23,6 +23,13 @@ Player: GrantConditionOnBotOwner@naval: Condition: enable-naval-ai Bots: naval + ResourceMapBotModule: + RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai + ResourceCreatorTypes: mine, gmine + ValuableResourceTypes: Ore, Gems + HarvesterTypes: harv + RefineryTypes: proc + EnemyBaseBuildingTypes: pbox,gun,ftur,tsla,agun,barr,tent,weap,afld,hpad,fact,powr,apwr,proc SupportPowerBotModule: RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai || enable-naval-ai Decisions: @@ -74,45 +81,55 @@ Player: Attractiveness: -10 TargetMetric: Value CheckRadius: 7c0 - HarvesterBotModule: - RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai || enable-naval-ai + HarvesterBotModule@rush-naval: + RequiresCondition: enable-rush-ai || enable-naval-ai HarvesterTypes: harv RefineryTypes: proc + InitialHarvesters: 2 + HarvesterBotModule@normal-turtle: + RequiresCondition: enable-normal-ai || enable-turtle-ai + HarvesterTypes: harv + RefineryTypes: proc + InitialHarvesters: 4 BaseBuilderBotModule@rush: RequiresCondition: enable-rush-ai - MinimumExcessPower: 60 + MinimumExcessPower: 0 MaximumExcessPower: 160 ExcessPowerIncrement: 40 ExcessPowerIncreaseThreshold: 4 ConstructionYardTypes: fact RefineryTypes: proc PowerTypes: powr,apwr - BarracksTypes: barr,tent - VehiclesFactoryTypes: weap + ExpansionTolerate: 0 + ForceExpansionTolerate: 0 + TechTypes: mslo, dome, atek, stek, fix + InititalMinimumRefineryCount: 0 + AdditionalMinimumRefineryCount: 2 ProductionTypes: barr,tent,weap + NewProductionCashThreshold: 7000 SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam BuildingLimits: - proc: 4 - barr: 1 - tent: 1 + barr: 7 + tent: 7 kenn: 1 dome: 1 - weap: 1 + weap: 4 atek: 1 stek: 1 fix: 1 BuildingFractions: - proc: 30 - barr: 1 + powr: 1 + proc: 1 + barr: 3 kenn: 1 - tent: 1 - weap: 1 - pbox: 7 - gun: 7 - tsla: 5 - gap: 2 - ftur: 10 + tent: 3 + weap: 4 + pbox: 3 + gun: 3 + tsla: 3 + gap: 1 + ftur: 3 agun: 5 sam: 5 atek: 1 @@ -121,28 +138,38 @@ Player: dome: 10 mslo: 1 BuildingDelays: - dome: 4500 + dome: 7000 + fix: 3000 + pbox: 2000 + gun: 3000 + ftur: 2000 + tsla: 3000 + kenn: 9000 + atek: 11000 + stek: 11000 BaseBuilderBotModule@normal: RequiresCondition: enable-normal-ai - MinimumExcessPower: 60 + MinimumExcessPower: 0 MaximumExcessPower: 200 ExcessPowerIncrement: 40 ExcessPowerIncreaseThreshold: 4 ConstructionYardTypes: fact RefineryTypes: proc + InititalMinimumRefineryCount: 0 + AdditionalMinimumRefineryCount: 2 + ExpansionTolerate: 1,2 PowerTypes: powr,apwr - BarracksTypes: barr,tent - VehiclesFactoryTypes: weap - ProductionTypes: barr,tent,weap,afld,hpad + TechTypes: dome, atek, stek, fix, afld, hpad + ProductionTypes: barr,tent,weap + NewProductionCashThreshold: 8000 NavalProductionTypes: spen, syrd SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam BuildingLimits: - proc: 4 - barr: 1 - tent: 1 + barr: 7 + tent: 7 dome: 1 - weap: 1 + weap: 4 spen: 1 syrd: 1 hpad: 4 @@ -152,19 +179,20 @@ Player: stek: 1 fix: 1 BuildingFractions: - proc: 15 - tent: 1 - barr: 1 + powr: 1 + proc: 1 + tent: 3 + barr: 3 kenn: 1 dome: 1 - weap: 6 - hpad: 4 + weap: 4 + hpad: 1 spen: 1 syrd: 1 - afld: 4 - afld.ukraine: 4 - pbox: 7 - gun: 7 + afld: 1 + afld.ukraine: 1 + pbox: 9 + gun: 9 ftur: 10 tsla: 5 gap: 2 @@ -175,24 +203,33 @@ Player: stek: 1 mslo: 1 BuildingDelays: - dome: 3000 + dome: 6000 + fix: 3000 + pbox: 1500 + gun: 2000 + ftur: 1500 + tsla: 2800 + kenn: 7000 + spen: 6000 + syrd: 6000 + atek: 9000 + stek: 9000 BaseBuilderBotModule@turtle: RequiresCondition: enable-turtle-ai - MinimumExcessPower: 60 + MinimumExcessPower: 20 MaximumExcessPower: 250 - ExcessPowerIncrement: 50 - ExcessPowerIncreaseThreshold: 4 + ExcessPowerIncrement: 20 + ExcessPowerIncreaseThreshold: 2 ConstructionYardTypes: fact RefineryTypes: proc PowerTypes: powr,apwr - BarracksTypes: barr,tent - VehiclesFactoryTypes: weap - ProductionTypes: barr,tent,weap,afld,hpad + ForceExpansionTolerate: 6, 7, 8 + TechTypes: dome, atek, stek, fix, afld, hpad + ProductionTypes: barr,tent,weap NavalProductionTypes: spen, syrd SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam BuildingLimits: - proc: 4 barr: 1 tent: 1 kenn: 1 @@ -207,7 +244,8 @@ Player: stek: 1 fix: 1 BuildingFractions: - proc: 30 + powr: 1 + proc: 1 tent: 1 barr: 1 kenn: 1 @@ -217,9 +255,9 @@ Player: afld.ukraine: 2 spen: 1 syrd: 1 - pbox: 10 - gun: 10 - ftur: 10 + pbox: 13 + gun: 12 + ftur: 13 tsla: 7 gap: 3 fix: 1 @@ -230,18 +268,22 @@ Player: stek: 1 mslo: 1 BuildingDelays: - dome: 3000 + dome: 5000 + atek: 7000 + stek: 7000 + kenn: 8000 BaseBuilderBotModule@naval: RequiresCondition: enable-naval-ai - MinimumExcessPower: 60 + MinimumExcessPower: 0 MaximumExcessPower: 400 ExcessPowerIncrement: 40 - ExcessPowerIncreaseThreshold: 4 + ExcessPowerIncreaseThreshold: 3 ConstructionYardTypes: fact RefineryTypes: proc PowerTypes: powr,apwr - BarracksTypes: barr,tent - VehiclesFactoryTypes: weap + ExpansionTolerate: 99 + ForceExpansionTolerate: 999 + TechTypes: mslo, dome, atek, stek, fix ProductionTypes: barr,tent,weap,afld,hpad NavalProductionTypes: spen, syrd SiloTypes: silo @@ -261,6 +303,7 @@ Player: stek: 1 fix: 1 BuildingFractions: + powr: 1 proc: 25 dome: 1 weap: 1 @@ -283,6 +326,7 @@ Player: dome: 3000 fix: 20000 tsla: 21000 + kenn: 4000 PowerDownBotModule: RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai || enable-naval-ai PowerDownTypes: dome,tsla,mslo,agun,sam @@ -298,11 +342,32 @@ Player: AircraftTargetType: AirborneActor ProtectionTypes: harv, mcv, mslo, gap, spen, syrd, iron, pdox, tsla, agun, dome, pbox, hbox, gun, ftur, sam, atek, weap, fact, proc, silo, hpad, afld, afld.ukraine, powr, apwr, stek, barr, kenn, tent, fix, fpwr, tenf, syrf, spef, weaf, domf, fixf, fapw, atef, pdof, mslf, facf IgnoredEnemyTargetTypes: AirborneActor - McvManagerBotModule: - RequiresCondition: enable-rush-ai || enable-normal-ai || enable-turtle-ai || enable-naval-ai + McvManagerBotModule@naval: + RequiresCondition: enable-naval-ai McvTypes: mcv ConstructionYardTypes: fact McvFactoryTypes: weap + McvExpansionManagerBotModule@rush: + RequiresCondition: enable-rush-ai + McvTypes: mcv + ConstructionYardTypes: fact + McvFactoryTypes: weap + MoveOldConyardFirst: true + MinimumConstructionYardCount: 2 + McvExpansionManagerBotModule@normal: + RequiresCondition: enable-normal-ai + McvTypes: mcv + ConstructionYardTypes: fact + McvFactoryTypes: weap + MinimumConstructionYardCount: 2 + McvExpansionManagerBotModule@turtle: + RequiresCondition: enable-turtle-ai + McvTypes: mcv + ConstructionYardTypes: fact + McvFactoryTypes: weap + MoveOldConyardFirst: false + MoveConyardTick: 8000 + MinimumConstructionYardCount: 2 UnitBuilderBotModule@rush: RequiresCondition: enable-rush-ai UnitsToBuild: @@ -310,6 +375,7 @@ Player: e2: 15 e3: 30 e4: 15 + e7: 1 dog: 15 shok: 15 harv: 10 @@ -340,6 +406,8 @@ Player: AircraftTargetType: AirborneActor ProtectionTypes: harv, mcv, mslo, gap, spen, syrd, iron, pdox, tsla, agun, dome, pbox, hbox, gun, ftur, sam, atek, weap, fact, proc, silo, hpad, afld, afld.ukraine, powr, apwr, stek, barr, kenn, tent, fix, fpwr, tenf, syrf, spef, weaf, domf, fixf, fapw, atef, pdof, mslf, facf IgnoredEnemyTargetTypes: AirborneActor + ProtectUnitScanRadius: 15 + ProtectionScanRadius: 12 UnitBuilderBotModule@normal: RequiresCondition: enable-normal-ai UnitsToBuild: @@ -347,6 +415,7 @@ Player: e2: 15 e3: 30 e4: 15 + e7: 1 dog: 15 shok: 15 harv: 15 @@ -386,6 +455,10 @@ Player: AircraftTargetType: AirborneActor ProtectionTypes: harv, mcv, mslo, gap, spen, syrd, iron, pdox, tsla, agun, dome, pbox, hbox, gun, ftur, sam, atek, weap, fact, proc, silo, hpad, afld, afld.ukraine, powr, apwr, stek, barr, kenn, tent, fix, fpwr, tenf, syrf, spef, weaf, domf, fixf, fapw, atef, pdof, mslf, facf IgnoredEnemyTargetTypes: AirborneActor + RushInterval: 100000 + MinimumAttackForceDelay: 100000 + ProtectUnitScanRadius: 30 + ProtectionScanRadius: 15 UnitBuilderBotModule@turtle: RequiresCondition: enable-turtle-ai UnitsToBuild: @@ -393,6 +466,7 @@ Player: e2: 15 e3: 30 e4: 15 + e7: 1 dog: 15 shok: 15 harv: 15 @@ -418,9 +492,9 @@ Player: pt: 10 mnly: 2 UnitLimits: - dog: 4 + dog: 3 harv: 8 - jeep: 4 + jeep: 2 ftrk: 4 mnly: 2 MinelayerBotModule@turtle: diff --git a/mods/ts/rules/ai.yaml b/mods/ts/rules/ai.yaml index cb4249df1c..cfcf5d0b15 100644 --- a/mods/ts/rules/ai.yaml +++ b/mods/ts/rules/ai.yaml @@ -9,6 +9,16 @@ Player: RequiresCondition: enable-test-ai HarvesterTypes: harv RefineryTypes: proc + PowerDownBotModule: + RequiresCondition: enable-test-ai + PowerDownTypes: garadr, naradr, gavulc,garock,gacsam,gactwr,naobel,nalasr,nasam + ResourceMapBotModule: + RequiresCondition: enable-test-ai + ResourceCreatorTypes: tibtre01, tibtre02, tibtre03, bigblue, bigblue3 + ValuableResourceTypes: Tiberium, BlueTiberium + HarvesterTypes: harv + RefineryTypes: proc + EnemyBaseBuildingTypes: gacnst, gapowr, gapowrup, napowr, naapwr, gapile, nahand, gaweap, naweap,garadr, naradr, gahpad, nahpad, gatech, natech, gavulc,garock,gacsam,gactwr,naobel,nalasr,nasam BaseBuilderBotModule@test: RequiresCondition: enable-test-ai MinimumExcessPower: 30 @@ -18,11 +28,11 @@ Player: ConstructionYardTypes: gacnst RefineryTypes: proc PowerTypes: gapowr, gapowrup, napowr, naapwr - BarracksTypes: gapile, nahand - VehiclesFactoryTypes: gaweap, naweap ProductionTypes: gapile, nahand, gaweap, naweap, gahpad, nahpad + TechTypes: garadr, naradr, gahpad, nahpad, gatech, natech SiloTypes: gasilo DefenseTypes: gavulc,garock,gacsam,gactwr,naobel,nalasr,nasam + SellRefineryNoResourceDistance: 18 BuildingLimits: proc: 4 gasilo: 2 @@ -105,8 +115,9 @@ Player: harv: 12 medic: 3 repair: 3 - McvManagerBotModule@test: + McvExpansionManagerBotModule@test: RequiresCondition: enable-test-ai McvTypes: mcv ConstructionYardTypes: gacnst McvFactoryTypes: gaweap, naweap + MinimumConstructionYardCount: 2