diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs index 81b827e0f5a0..45411ffc395c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBuilderBotModule.cs @@ -53,6 +53,12 @@ public class BaseBuilderBotModuleInfo : ConditionalTraitInfo [Desc("Production queues AI uses for defenses.")] public readonly HashSet DefenseQueues = new() { "Defense" }; + [Desc("Tells the AI what building types are considered barriers.")] + public readonly HashSet WallTypes = new(); + + [Desc("Buildings that should be walled off, if any.")] + public readonly HashSet WalledStructures = new(); + [Desc("Minimum distance in cells from center of the base when checking for building placement.")] public readonly int MinBaseRadius = 2; @@ -334,7 +340,7 @@ CPos ChooseRallyLocationNear(Actor producer) if (possibleRallyPoints.Count == 0) { - AIUtils.BotDebug("{0} has no possible rallypoint near {1}", producer.Owner, producer.Location); + AIUtils.BotDebug($"{producer.Owner} has no possible rallypoint near {producer.Location}"); return producer.Location; } diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 0acbc22c93e3..68d6dc4d7ebf 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -168,6 +168,11 @@ bool TickQueue(IBot bot, ProductionQueue queue) location = possibleBuilding.Actor.Location + possibleBuilding.Trait.Info.Offset; } } + else if (actorInfo.HasTraitInfo()) + { + orderString = "LineBuild"; + location = ChooseWallLocation(actorInfo).Location; + } else { // Check if Building is a defense and if we should place it towards the enemy or not. @@ -254,7 +259,7 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) if (playerPower != null && playerPower.ExcessPower < minimumExcessPower && power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(p => p.Amount) > 0) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (low power)"); return power; } @@ -264,30 +269,44 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) var refinery = GetProducibleBuilding(baseBuilder.Info.RefineryTypes, buildableThings); if (refinery != null && HasSufficientPowerForActor(refinery)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (refinery)", queue.Actor.Owner, refinery.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {refinery.Name}: Priority override (refinery)"); return refinery; } if (power != null && refinery != null && !HasSufficientPowerForActor(refinery)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (would be low power)"); return power; } } + if (baseBuilder.Info.WalledStructures.Count > 0) + { + var wall = GetProducibleBuilding(baseBuilder.Info.WallTypes, buildableThings); + if (wall != null && HasSufficientPowerForActor(wall)) + { + var possibleLocation = ChooseWallLocation(wall); + if (possibleLocation.Location != null) + { + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {wall.Name}: Priority override (wall)"); + return wall; + } + } + } + // Make sure that we can spend as fast as we are earning if (baseBuilder.Info.NewProductionCashThreshold > 0 && playerResources.GetCashAndResources() > baseBuilder.Info.NewProductionCashThreshold) { var production = GetProducibleBuilding(baseBuilder.Info.ProductionTypes, buildableThings); if (production != null && HasSufficientPowerForActor(production)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (production)", queue.Actor.Owner, production.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {production.Name}: Priority override (production)"); return production; } if (power != null && production != null && !HasSufficientPowerForActor(production)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (would be low power)"); return power; } } @@ -300,13 +319,13 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) var navalproduction = GetProducibleBuilding(baseBuilder.Info.NavalProductionTypes, buildableThings); if (navalproduction != null && HasSufficientPowerForActor(navalproduction)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (navalproduction)", queue.Actor.Owner, navalproduction.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {navalproduction.Name}: Priority override (navalproduction)"); return navalproduction; } if (power != null && navalproduction != null && !HasSufficientPowerForActor(navalproduction)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (would be low power)"); return power; } } @@ -317,13 +336,13 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) var silo = GetProducibleBuilding(baseBuilder.Info.SiloTypes, buildableThings); if (silo != null && HasSufficientPowerForActor(silo)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (silo)", queue.Actor.Owner, silo.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {silo.Name}: Priority override (silo)"); return silo; } if (power != null && silo != null && !HasSufficientPowerForActor(silo)) { - AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (would be low power)"); return power; } } @@ -375,9 +394,9 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) if (power != null && power.TraitInfos().Where(i => i.EnabledByDefault).Sum(pi => pi.Amount) > 0) { if (playerPower.PowerOutageRemainingTicks > 0) - AIUtils.BotDebug("{0} decided to build {1}: Priority override (is low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (is low power)"); else - AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name); + AIUtils.BotDebug($"{queue.Actor.Owner} decided to build {power.Name}: Priority override (would be low power)"); return power; } @@ -390,16 +409,45 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) } // Too spammy to keep enabled all the time, but very useful when debugging specific issues. - // AIUtils.BotDebug("{0} couldn't decide what to build for queue {1}.", queue.Actor.Owner, queue.Info.Group); + // AIUtils.BotDebug($"{queue.Actor.Owner} couldn't decide what to build for queue {queue.Info.Group}."); return null; } + (CPos? Location, int Variant) ChooseWallLocation(ActorInfo actorInfo) + { + // Build around important structure + var buildingToWall = world.ActorsWithTrait().FirstOrDefault(a => !a.Actor.Disposed && a.Actor.Owner == player && + baseBuilder.Info.WalledStructures.Contains(a.Actor.Info.Name)); + + if (buildingToWall.Actor == null) + return (null, 0); + + var topLeft = buildingToWall.Actor.Location; + var edges = new List() + { + topLeft + new CVec(-1, -1), + topLeft + new CVec(buildingToWall.Trait.Info.Dimensions.X, -1), + topLeft + new CVec(-1, buildingToWall.Trait.Info.Dimensions.Y), + topLeft + buildingToWall.Trait.Info.Dimensions, + }; + + var buildingInfo = actorInfo.TraitInfoOrDefault(); + if (buildingInfo == null) + return (null, 0); + + var possibleEdges = edges.Where(e => world.CanPlaceBuilding(e, actorInfo, buildingInfo, null)); + if (!possibleEdges.Any()) + return (null, 0); + + return (possibleEdges.Random(world.LocalRandom), 0); + } + (CPos? Location, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type) { var actorInfo = world.Map.Rules.Actors[actorType]; - var bi = actorInfo.TraitInfoOrDefault(); + var buildingInfo = actorInfo.TraitInfoOrDefault(); - if (bi == null) + if (buildingInfo == null) return (null, 0); // Find the buildable cell that is closest to pos and centered around center @@ -407,8 +455,8 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) { var actorVariant = 0; var buildingVariantInfo = actorInfo.TraitInfoOrDefault(); - var variantActorInfo = actorInfo; - var vbi = bi; + var actorInfoVariant = actorInfo; + var buildingInfoVariant = buildingInfo; var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); @@ -458,16 +506,16 @@ ActorInfo ChooseBuildingToBuild(ProductionQueue queue) if (actorVariant != 0) { - variantActorInfo = world.Map.Rules.Actors[buildingVariantInfo.Actors[actorVariant - 1]]; - vbi = variantActorInfo.TraitInfoOrDefault(); + actorInfoVariant = world.Map.Rules.Actors[buildingVariantInfo.Actors[actorVariant - 1]]; + buildingInfoVariant = actorInfoVariant.TraitInfoOrDefault(); } foreach (var cell in cells) { - if (!world.CanPlaceBuilding(cell, variantActorInfo, vbi, null)) + if (!world.CanPlaceBuilding(cell, actorInfoVariant, buildingInfoVariant, null)) continue; - if (distanceToBaseIsImportant && !vbi.IsCloseEnoughToBase(world, player, variantActorInfo, cell)) + if (distanceToBaseIsImportant && !buildingInfoVariant.IsCloseEnoughToBase(world, player, actorInfoVariant, cell)) continue; return (cell, actorVariant); diff --git a/mods/ra/rules/ai.yaml b/mods/ra/rules/ai.yaml index c40e8f6d8898..aa7c0f1e42db 100644 --- a/mods/ra/rules/ai.yaml +++ b/mods/ra/rules/ai.yaml @@ -98,6 +98,8 @@ Player: ProductionTypes: barr,tent,weap SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam + WallTypes: brik + WalledStructures: fact BuildingLimits: proc: 4 barr: 1 @@ -143,14 +145,16 @@ Player: NavalProductionTypes: spen, syrd SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam + WallTypes: brik + WalledStructures: fact BuildingLimits: proc: 6 barr: 3 tent: 3 dome: 1 weap: 3 - spen: 2 - syrd: 2 + spen: 1 + syrd: 1 hpad: 4 afld: 4 afld.ukraine: 4 @@ -197,6 +201,8 @@ Player: NavalProductionTypes: spen, syrd SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam + WallTypes: brik + WalledStructures: fact BuildingLimits: proc: 4 barr: 1 @@ -252,6 +258,8 @@ Player: NavalProductionTypes: spen, syrd SiloTypes: silo DefenseTypes: hbox,pbox,gun,ftur,tsla,agun,sam + WallTypes: brik + WalledStructures: fact BuildingLimits: proc: 3 dome: 1