1. Only allow new item being queued when cash above a certain number 2. Only tick one kind of queues at one tick, reduce the pressure on the actived tick 3. 'BaseBuilderBotModule' will check all buildings in producing, avoid queue mutiple same buildings.
519 lines
19 KiB
C#
519 lines
19 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
sealed class BaseBuilderQueueManager
|
|
{
|
|
public readonly string Category;
|
|
public int WaitTicks;
|
|
|
|
readonly BaseBuilderBotModule baseBuilder;
|
|
readonly World world;
|
|
readonly Player player;
|
|
readonly PowerManager playerPower;
|
|
readonly PlayerResources playerResources;
|
|
readonly IResourceLayer resourceLayer;
|
|
|
|
Actor[] playerBuildings;
|
|
int failCount;
|
|
int failRetryTicks;
|
|
int checkForBasesTicks;
|
|
int cachedBases;
|
|
int cachedBuildings;
|
|
int minimumExcessPower;
|
|
|
|
bool itemQueuedThisTick = false;
|
|
|
|
WaterCheck waterState = WaterCheck.NotChecked;
|
|
|
|
public BaseBuilderQueueManager(BaseBuilderBotModule baseBuilder, string category, Player p, PowerManager pm,
|
|
PlayerResources pr, IResourceLayer rl)
|
|
{
|
|
this.baseBuilder = baseBuilder;
|
|
world = p.World;
|
|
player = p;
|
|
playerPower = pm;
|
|
playerResources = pr;
|
|
resourceLayer = rl;
|
|
Category = category;
|
|
failRetryTicks = baseBuilder.Info.StructureProductionResumeDelay;
|
|
minimumExcessPower = baseBuilder.Info.MinimumExcessPower;
|
|
if (baseBuilder.Info.NavalProductionTypes.Count == 0)
|
|
waterState = WaterCheck.DontCheck;
|
|
}
|
|
|
|
public void Tick(IBot bot)
|
|
{
|
|
// If failed to place something N consecutive times, wait M ticks until resuming building production
|
|
if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts && --failRetryTicks <= 0)
|
|
{
|
|
var currentBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
|
|
var baseProviders = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (waterState == WaterCheck.NotChecked)
|
|
{
|
|
if (AIUtils.IsAreaAvailable<BaseProvider>(world, player, world.Map, baseBuilder.Info.MaxBaseRadius, baseBuilder.Info.WaterTerrainTypes))
|
|
waterState = WaterCheck.EnoughWater;
|
|
else
|
|
{
|
|
waterState = WaterCheck.NotEnoughWater;
|
|
checkForBasesTicks = baseBuilder.Info.CheckForNewBasesDelay;
|
|
}
|
|
}
|
|
|
|
if (waterState == WaterCheck.NotEnoughWater && --checkForBasesTicks <= 0)
|
|
{
|
|
var currentBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
|
|
|
if (currentBases > cachedBases)
|
|
{
|
|
cachedBases = currentBases;
|
|
waterState = WaterCheck.NotChecked;
|
|
}
|
|
}
|
|
|
|
// Only update once per second or so
|
|
if (WaitTicks > 0)
|
|
return;
|
|
|
|
playerBuildings = world.ActorsHavingTrait<Building>().Where(a => a.Owner == player).ToArray();
|
|
var excessPowerBonus = baseBuilder.Info.ExcessPowerIncrement * (playerBuildings.Length / baseBuilder.Info.ExcessPowerIncreaseThreshold.Clamp(1, int.MaxValue));
|
|
minimumExcessPower = (baseBuilder.Info.MinimumExcessPower + excessPowerBonus).Clamp(baseBuilder.Info.MinimumExcessPower, baseBuilder.Info.MaximumExcessPower);
|
|
|
|
// PERF: Queue only one actor at a time per category
|
|
itemQueuedThisTick = false;
|
|
var active = false;
|
|
foreach (var queue in AIUtils.FindQueues(player, Category))
|
|
{
|
|
if (TickQueue(bot, queue))
|
|
active = true;
|
|
}
|
|
|
|
// Add a random factor so not every AI produces at the same tick early in the game.
|
|
// Minimum should not be negative as delays in HackyAI could be zero.
|
|
var randomFactor = world.LocalRandom.Next(0, baseBuilder.Info.StructureProductionRandomBonusDelay);
|
|
|
|
WaitTicks = active ? baseBuilder.Info.StructureProductionActiveDelay + randomFactor
|
|
: baseBuilder.Info.StructureProductionInactiveDelay + randomFactor;
|
|
}
|
|
|
|
bool TickQueue(IBot bot, ProductionQueue queue)
|
|
{
|
|
var currentBuilding = queue.AllQueued().FirstOrDefault();
|
|
|
|
// Waiting to build something
|
|
if (currentBuilding == null && failCount < baseBuilder.Info.MaximumFailedPlacementAttempts)
|
|
{
|
|
// PERF: We shouldn't be queueing new units when we're low on cash
|
|
if (playerResources.GetCashAndResources() < baseBuilder.Info.ProductionMinCashRequirement || itemQueuedThisTick)
|
|
return false;
|
|
|
|
var item = ChooseBuildingToBuild(queue);
|
|
if (item == null)
|
|
return false;
|
|
|
|
bot.QueueOrder(Order.StartProduction(queue.Actor, item.Name, 1));
|
|
itemQueuedThisTick = true;
|
|
}
|
|
else if (currentBuilding != null && currentBuilding.Done)
|
|
{
|
|
// Production is complete
|
|
// Choose the placement logic
|
|
// HACK: HACK HACK HACK
|
|
// TODO: Derive this from BuildingCommonNames instead
|
|
var type = BuildingType.Building;
|
|
CPos? location = null;
|
|
var actorVariant = 0;
|
|
var orderString = "PlaceBuilding";
|
|
|
|
// Check if Building is a plug for other Building
|
|
var actorInfo = world.Map.Rules.Actors[currentBuilding.Item];
|
|
var plugInfo = actorInfo.TraitInfoOrDefault<PlugInfo>();
|
|
|
|
if (plugInfo != null)
|
|
{
|
|
var possibleBuilding = world.ActorsWithTrait<Pluggable>().FirstOrDefault(a =>
|
|
a.Actor.Owner == player && a.Trait.AcceptsPlug(plugInfo.Type));
|
|
|
|
if (possibleBuilding.Actor != null)
|
|
{
|
|
orderString = "PlacePlug";
|
|
location = possibleBuilding.Actor.Location + possibleBuilding.Trait.Info.Offset;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check if Building is a defense and if we should place it towards the enemy or not.
|
|
if (baseBuilder.Info.DefenseTypes.Contains(actorInfo.Name) && world.LocalRandom.Next(100) < baseBuilder.Info.PlaceDefenseTowardsEnemyChance)
|
|
type = BuildingType.Defense;
|
|
else if (baseBuilder.Info.RefineryTypes.Contains(actorInfo.Name))
|
|
type = BuildingType.Refinery;
|
|
|
|
(location, 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)
|
|
{
|
|
cachedBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
|
|
cachedBases = world.ActorsHavingTrait<BaseProvider>().Count(a => a.Owner == player);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
failCount = 0;
|
|
|
|
bot.QueueOrder(new Order(orderString, player.PlayerActor, Target.FromCell(world, location.Value), false)
|
|
{
|
|
// Building to place
|
|
TargetString = currentBuilding.Item,
|
|
|
|
// Actor variant will always be small enough to safely pack in a CPos
|
|
ExtraLocation = new CPos(actorVariant, 0),
|
|
|
|
// Actor ID to associate the placement with
|
|
ExtraData = queue.Actor.ActorID,
|
|
SuppressVisualFeedback = true
|
|
});
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ActorInfo GetProducibleBuilding(HashSet<string> actors, IEnumerable<ActorInfo> buildables, Func<ActorInfo, int> orderBy = null)
|
|
{
|
|
var available = buildables.Where(actor =>
|
|
{
|
|
// Are we able to build this?
|
|
if (!actors.Contains(actor.Name))
|
|
return false;
|
|
|
|
if (!baseBuilder.Info.BuildingLimits.ContainsKey(actor.Name))
|
|
return true;
|
|
|
|
return playerBuildings.Count(a => a.Info.Name == actor.Name) < baseBuilder.Info.BuildingLimits[actor.Name];
|
|
});
|
|
|
|
if (orderBy != null)
|
|
return available.MaxByOrDefault(orderBy);
|
|
|
|
return available.RandomOrDefault(world.LocalRandom);
|
|
}
|
|
|
|
bool HasSufficientPowerForActor(ActorInfo actorInfo)
|
|
{
|
|
return playerPower == null || actorInfo.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault)
|
|
.Sum(p => p.Amount) + playerPower.ExcessPower >= baseBuilder.Info.MinimumExcessPower;
|
|
}
|
|
|
|
ActorInfo ChooseBuildingToBuild(ProductionQueue queue)
|
|
{
|
|
var buildableThings = queue.BuildableItems().ToList();
|
|
|
|
// This gets used quite a bit, so let's cache it here
|
|
var power = GetProducibleBuilding(baseBuilder.Info.PowerTypes, buildableThings,
|
|
a => a.TraitInfos<PowerInfo>().Where(i => i.EnabledByDefault).Sum(p => p.Amount));
|
|
|
|
// First priority is to get out of a low power situation
|
|
if (playerPower != null && playerPower.ExcessPower < minimumExcessPower)
|
|
{
|
|
if (power != null && power.TraitInfos<PowerInfo>().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);
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// Next is to build up a strong economy
|
|
if (!baseBuilder.HasAdequateRefineryCount)
|
|
{
|
|
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);
|
|
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);
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
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);
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// Only consider building this if there is enough water inside the base perimeter and there are close enough adjacent buildings
|
|
if (waterState == WaterCheck.EnoughWater && baseBuilder.Info.NewProductionCashThreshold > 0
|
|
&& playerResources.GetCashAndResources() > baseBuilder.Info.NewProductionCashThreshold
|
|
&& AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes))
|
|
{
|
|
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);
|
|
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);
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// Create some head room for resource storage if we really need it
|
|
if (playerResources.Resources > 0.8 * playerResources.ResourceCapacity)
|
|
{
|
|
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);
|
|
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);
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// Build everything else
|
|
foreach (var frac in baseBuilder.Info.BuildingFractions.Shuffle(world.LocalRandom))
|
|
{
|
|
var name = frac.Key;
|
|
|
|
// Does this building have initial delay, if so have we passed it?
|
|
if (baseBuilder.Info.BuildingDelays != null &&
|
|
baseBuilder.Info.BuildingDelays.TryGetValue(name, out var delay) &&
|
|
delay > world.WorldTick)
|
|
continue;
|
|
|
|
// Can we build this structure?
|
|
if (!buildableThings.Any(b => b.Name == name))
|
|
continue;
|
|
|
|
// Check the number of this structure and its variants
|
|
var actorInfo = world.Map.Rules.Actors[name];
|
|
var buildingVariantInfo = actorInfo.TraitInfoOrDefault<PlaceBuildingVariantsInfo>();
|
|
var variants = buildingVariantInfo?.Actors ?? Array.Empty<string>();
|
|
|
|
var count = playerBuildings.Count(a => a.Info.Name == name || variants.Contains(a.Info.Name)) + (baseBuilder.BuildingsBeingProduced.TryGetValue(name, out var num) ? num : 0);
|
|
|
|
// Do we want to build this structure?
|
|
if (count * 100 > frac.Value * playerBuildings.Length)
|
|
continue;
|
|
|
|
if (baseBuilder.Info.BuildingLimits.TryGetValue(name, out var limit) && limit <= count)
|
|
continue;
|
|
|
|
// If we're considering to build a naval structure, check whether there is enough water inside the base perimeter
|
|
// and any structure providing buildable area close enough to that water.
|
|
// TODO: Extend this check to cover any naval structure, not just production.
|
|
if (baseBuilder.Info.NavalProductionTypes.Contains(name)
|
|
&& (waterState == WaterCheck.NotEnoughWater
|
|
|| !AIUtils.IsAreaAvailable<GivesBuildableArea>(world, player, world.Map, baseBuilder.Info.CheckForWaterRadius, baseBuilder.Info.WaterTerrainTypes)))
|
|
continue;
|
|
|
|
// Will this put us into low power?
|
|
var actor = world.Map.Rules.Actors[name];
|
|
if (playerPower != null && (playerPower.ExcessPower < minimumExcessPower || !HasSufficientPowerForActor(actor)))
|
|
{
|
|
// Try building a power plant instead
|
|
if (power != null && power.TraitInfos<PowerInfo>().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);
|
|
else
|
|
AIUtils.BotDebug("{0} decided to build {1}: Priority override (would be low power)", queue.Actor.Owner, power.Name);
|
|
|
|
return power;
|
|
}
|
|
}
|
|
|
|
// Lets build this
|
|
AIUtils.BotDebug("{0} decided to build {1}: Desired is {2} ({3} / {4}); current is {5} / {4}",
|
|
queue.Actor.Owner, name, frac.Value, frac.Value * playerBuildings.Length, playerBuildings.Length, count);
|
|
return actor;
|
|
}
|
|
|
|
// 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);
|
|
return null;
|
|
}
|
|
|
|
(CPos? Location, int Variant) ChooseBuildLocation(string actorType, bool distanceToBaseIsImportant, BuildingType type)
|
|
{
|
|
var actorInfo = world.Map.Rules.Actors[actorType];
|
|
var bi = actorInfo.TraitInfoOrDefault<BuildingInfo>();
|
|
|
|
if (bi == null)
|
|
return (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)
|
|
{
|
|
var actorVariant = 0;
|
|
var buildingVariantInfo = actorInfo.TraitInfoOrDefault<PlaceBuildingVariantsInfo>();
|
|
var variantActorInfo = actorInfo;
|
|
var vbi = bi;
|
|
|
|
var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange);
|
|
|
|
// Sort by distance to target if we have one
|
|
if (center != target)
|
|
{
|
|
cells = cells.OrderBy(c => (c - target).LengthSquared);
|
|
|
|
// Rotate building if we have a Facings in buildingVariantInfo.
|
|
// If we don't have Facings in buildingVariantInfo, use a random variant
|
|
if (buildingVariantInfo?.Actors != null)
|
|
{
|
|
if (buildingVariantInfo.Facings != null)
|
|
{
|
|
var vector = world.Map.CenterOfCell(target) - world.Map.CenterOfCell(center);
|
|
|
|
// The rotation Y point to upside vertically, so -Y = Y(rotation)
|
|
var desireFacing = new WAngle(WAngle.ArcSin((int)((long)Math.Abs(vector.X) * 1024 / vector.Length)).Angle);
|
|
if (vector.X > 0 && vector.Y >= 0)
|
|
desireFacing = new WAngle(512) - desireFacing;
|
|
else if (vector.X < 0 && vector.Y >= 0)
|
|
desireFacing = new WAngle(512) + desireFacing;
|
|
else if (vector.X < 0 && vector.Y < 0)
|
|
desireFacing = -desireFacing;
|
|
|
|
for (int i = 0, e = 1024; i < buildingVariantInfo.Facings.Length; i++)
|
|
{
|
|
var minDelta = Math.Min((desireFacing - buildingVariantInfo.Facings[i]).Angle, (buildingVariantInfo.Facings[i] - desireFacing).Angle);
|
|
if (e > minDelta)
|
|
{
|
|
e = minDelta;
|
|
actorVariant = i;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
actorVariant = world.LocalRandom.Next(buildingVariantInfo.Actors.Length + 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
cells = cells.Shuffle(world.LocalRandom);
|
|
|
|
if (buildingVariantInfo?.Actors != null)
|
|
actorVariant = world.LocalRandom.Next(buildingVariantInfo.Actors.Length + 1);
|
|
}
|
|
|
|
if (actorVariant != 0)
|
|
{
|
|
variantActorInfo = world.Map.Rules.Actors[buildingVariantInfo.Actors[actorVariant - 1]];
|
|
vbi = variantActorInfo.TraitInfoOrDefault<BuildingInfo>();
|
|
}
|
|
|
|
foreach (var cell in cells)
|
|
{
|
|
if (!world.CanPlaceBuilding(cell, variantActorInfo, vbi, null))
|
|
continue;
|
|
|
|
if (distanceToBaseIsImportant && !vbi.IsCloseEnoughToBase(world, player, variantActorInfo, cell))
|
|
continue;
|
|
|
|
return (cell, actorVariant);
|
|
}
|
|
|
|
return (null, 0);
|
|
}
|
|
|
|
var baseCenter = baseBuilder.GetRandomBaseCenter();
|
|
|
|
switch (type)
|
|
{
|
|
case BuildingType.Defense:
|
|
|
|
// Build near the closest enemy structure
|
|
var closestEnemy = world.ActorsHavingTrait<Building>().Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy)
|
|
.ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
|
|
|
|
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;
|
|
|
|
return FindPos(baseBuilder.DefenseCenter, targetCell, baseBuilder.Info.MinimumDefenseRadius, baseBuilder.Info.MaximumDefenseRadius);
|
|
|
|
case BuildingType.Refinery:
|
|
|
|
// 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);
|
|
|
|
foreach (var r in nearbyResources)
|
|
{
|
|
var found = FindPos(baseCenter, r, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
|
|
if (found.Location != null)
|
|
return found;
|
|
}
|
|
}
|
|
|
|
// Try and find a free spot somewhere else in the base
|
|
return FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius, baseBuilder.Info.MaxBaseRadius);
|
|
|
|
case BuildingType.Building:
|
|
return FindPos(baseCenter, baseCenter, baseBuilder.Info.MinBaseRadius,
|
|
distanceToBaseIsImportant ? baseBuilder.Info.MaxBaseRadius : world.Map.Grid.MaximumTileSearchRange);
|
|
}
|
|
|
|
// Can't find a build location
|
|
return (null, 0);
|
|
}
|
|
}
|
|
}
|