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