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

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

View File

@@ -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<IResourceLayerInfo>
public class HarvesterBotModuleInfo : ConditionalTraitInfo, NotBefore<IResourceLayerInfo>, NotBefore<ResourceMapBotModuleInfo>
{
[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<HarvesterBotModuleInfo>, IBotTick, INotifyActorDisposing, IWorldLoaded
public class HarvesterBotModule : ConditionalTrait<HarvesterBotModuleInfo>, 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<ResourceMapBotModule>().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<Actor>();
foreach (var harv in harvesters)
{
if (needHarvs <= 0 || harvestersCanAssign <= 0)
break;
var parach = harv.TraitOrDefault<Parachutable>();
if (parach != null && parach.IsInAir)
{
harvestersCanAssign--;
usedHarvs.Add(harv);
continue;
}
var mobile = harv.TraitOrDefault<Mobile>();
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<Harvester>().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<Parachutable>();
if (parach != null && parach.IsInAir)
return;
var dockClientManager = self.Trait<DockClientManager>();
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));
}
}
}