Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
#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.Frozen;
|
||||
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;
|
||||
CPos? baseCenterKeepsFailing = null;
|
||||
|
||||
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;
|
||||
minimumExcessPower = baseBuilder.Info.MinimumExcessPower;
|
||||
if (baseBuilder.Info.NavalProductionTypes.Count == 0)
|
||||
waterState = WaterCheck.DontCheck;
|
||||
}
|
||||
|
||||
public void Tick(IBot bot, ILookup<string, ProductionQueue> queuesByCategory)
|
||||
{
|
||||
// If we can't place any structures, give a nudge to BaseExpansionModules and hope it gets fixed.
|
||||
if (failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts)
|
||||
{
|
||||
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);
|
||||
|
||||
if (stuckConyard != null)
|
||||
{
|
||||
foreach (var be in baseBuilder.BaseExpansionModules)
|
||||
be.UpdateExpansionParams(bot, false, true, stuckConyard);
|
||||
}
|
||||
|
||||
failCount = 0;
|
||||
}
|
||||
|
||||
// No BaseExpansionModules exist. Only bother resetting failCount when 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.
|
||||
else if (baseBuilder.BaseExpansionModules == null && --failRetryTicks <= 0)
|
||||
{
|
||||
var currentBuildings = world.ActorsHavingTrait<Building>().Count(a => a.Owner == player);
|
||||
var baseProviders = world.ActorsHavingTrait<BaseProvider>().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)
|
||||
{
|
||||
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 queuesByCategory[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, baseCenterKeepsFailing, actorVariant) = ChooseBuildLocation(currentBuilding.Item, true, type);
|
||||
}
|
||||
|
||||
if (location == null)
|
||||
{
|
||||
// If we just reached the maximum fail count, cache the number of current structures
|
||||
if (++failCount >= baseBuilder.Info.MaximumFailedPlacementAttempts)
|
||||
{
|
||||
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<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
|
||||
});
|
||||
|
||||
// After succesfuly placing a building, nudge BaseExpansionModules to expand.
|
||||
// We want to avoid expanding too often, so we make a judgement by counting buildings.
|
||||
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);
|
||||
|
||||
var tolerateOnCash = playerResources.GetCashAndResources() / Math.Max(baseBuilder.Info.PerExpansionTolerateOnCash, 1);
|
||||
|
||||
if (numRef >= baseBuilder.Info.InititalMinimumRefineryCount + baseBuilder.Info.AdditionalMinimumRefineryCount
|
||||
&& numProd > 0 && numProd + numTech - baseBuilder.Info.ExpansionTolerate.Random(world.LocalRandom) - tolerateOnCash >= numRef)
|
||||
{
|
||||
var undeployEvenNoBase = numProd + numTech - baseBuilder.Info.ForceExpansionTolerate.Random(world.LocalRandom) - tolerateOnCash >= numRef;
|
||||
|
||||
foreach (var be in baseBuilder.BaseExpansionModules)
|
||||
be.UpdateExpansionParams(bot, true, undeployEvenNoBase, null);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ActorInfo GetProducibleBuilding(FrozenSet<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.TryGetValue(actor.Name, out var limit))
|
||||
return true;
|
||||
|
||||
return playerBuildings.Count(a => a.Info.Name == actor.Name) < limit;
|
||||
});
|
||||
|
||||
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 &&
|
||||
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.RequestedRefineries.Count > 0 || !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
|
||||
&& world.LocalRandom.Next(100) < baseBuilder.Info.NewProductionChance)
|
||||
{
|
||||
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 ?? [];
|
||||
|
||||
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, CPos? BaseCenter, 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, null, 0);
|
||||
|
||||
// Find the buildable cell that is closest to pos and centered around center
|
||||
(CPos? Location, CPos Center, 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, center, actorVariant);
|
||||
}
|
||||
|
||||
return (null, center, 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)
|
||||
.ClosestToIgnoringPath(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:
|
||||
|
||||
var requestRef = baseBuilder.RequestedRefineries.Count > 0 ? baseBuilder.RequestedRefineries.Keys.First() : null;
|
||||
|
||||
// Try and place the refinery near a resource field
|
||||
if (resourceLayer != null)
|
||||
{
|
||||
// If we have failed to place to the requested refinery point, try and place it near the base center
|
||||
var resourceBaseCenter = failCount > 0 ? baseCenter :
|
||||
(requestRef != null ? baseBuilder.RequestedRefineries[requestRef].ConyardLoc : (baseBuilder.ResourceConyardCenter ?? baseCenter));
|
||||
|
||||
// If we have a ResourceMapModule, only consider the resource types it considers valuable
|
||||
// Otherwise consider any resource type
|
||||
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);
|
||||
|
||||
// Find the closest refinery we have if we have any when not failing to place for the first time
|
||||
var closestRefinery = failCount <= 0
|
||||
? baseBuilder.RefineryBuildings.Actors.Where(a => !a.IsDead)?.ClosestToIgnoringPath(world.Map.CenterOfCell(resourceBaseCenter))
|
||||
: null;
|
||||
|
||||
IEnumerable<CPos> resourcesShouldCheck = null;
|
||||
|
||||
if (closestRefinery == null)
|
||||
resourcesShouldCheck = nearbyResources.Shuffle(world.LocalRandom).Take(baseBuilder.Info.MaxResourceCellsToCheck);
|
||||
else if (requestRef != null)
|
||||
{
|
||||
resourcesShouldCheck = nearbyResources.OrderBy(c => (c - baseBuilder.RequestedRefineries[requestRef].ResourceLoc).LengthSquared)
|
||||
.Take(baseBuilder.Info.MaxResourceCellsToCheck);
|
||||
}
|
||||
else
|
||||
resourcesShouldCheck = nearbyResources.OrderByDescending(c => (c - closestRefinery.Location).LengthSquared)
|
||||
.Take(baseBuilder.Info.MaxResourceCellsToCheck);
|
||||
|
||||
foreach (var r in resourcesShouldCheck)
|
||||
{
|
||||
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);
|
||||
|
||||
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, null, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
#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.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[TraitLocation(SystemActors.Player)]
|
||||
[Desc("Manages AI minelayer unit related with " + nameof(Minelayer) + " traits.",
|
||||
"When enemy damage AI's actors, the location of conflict will be recorded,",
|
||||
"If a location is a valid spot, it will add/merge to favorite location for usage later")]
|
||||
public class MinelayerBotModuleInfo : ConditionalTraitInfo
|
||||
{
|
||||
[Desc("Enemy target types to ignore when add the minefield location to conflict location.")]
|
||||
public readonly BitSet<TargetableType> IgnoredEnemyTargetTypes = default;
|
||||
|
||||
[Desc("Victim target types that considering conflict location as enemy location instead of victim location.")]
|
||||
public readonly BitSet<TargetableType> UseEnemyLocationTargetTypes = default;
|
||||
|
||||
[ActorReference(typeof(MinelayerInfo))]
|
||||
[Desc("Actors with " + nameof(Minelayer) + "trait.")]
|
||||
public readonly FrozenSet<string> MinelayingActorTypes = default;
|
||||
|
||||
[Desc("Find this amount of suitable actors and lay mine to a location.")]
|
||||
public readonly int MaxPerAssign = 1;
|
||||
|
||||
[Desc("Scan suitable actors and target in this interval.")]
|
||||
public readonly int ScanTick = 320;
|
||||
|
||||
[Desc("Radius per mine laying order.")]
|
||||
public readonly int MineFieldRadius = 1;
|
||||
|
||||
[Desc("Minefield location is cancelled if those whose target type belong to allied nearby.")]
|
||||
public readonly BitSet<TargetableType> AwayFromAlliedTargetTypes = default;
|
||||
|
||||
[Desc("Minefield location is cancelled if those whose target type belong to enemy nearby.")]
|
||||
public readonly BitSet<TargetableType> AwayFromEnemyTargetTypes = default;
|
||||
|
||||
[Desc("Minefield location check distance to AwayFromAlliedTargettype and AwayFromEnemyTargettype.",
|
||||
"In addition, if any enemy actor within this range and minefield location is not cancelled,",
|
||||
"minelayer will try lay mines at the 3/4 path to minefield location")]
|
||||
public readonly int AwayFromCellDistance = 9;
|
||||
|
||||
[Desc("Merge conflict point minefield position to a favorite minefield position if within this range and closest.",
|
||||
"If favorite minefield positions is at the max of 5, we always merge it to closest regardless of this")]
|
||||
public readonly int FavoritePositionDistance = 6;
|
||||
|
||||
public override object Create(ActorInitializer init) { return new MinelayerBotModule(init.Self, this); }
|
||||
}
|
||||
|
||||
public class MinelayerBotModule : ConditionalTrait<MinelayerBotModuleInfo>, IBotTick, IBotRespondToAttack
|
||||
{
|
||||
const int MaxPositionCacheLength = 5;
|
||||
const int RepeatedAltertTicks = 40;
|
||||
|
||||
readonly World world;
|
||||
readonly Player player;
|
||||
readonly Predicate<Actor> unitCannotBeOrdered;
|
||||
readonly Predicate<Actor> unitCannotBeOrderedOrIsBusy;
|
||||
readonly CPos?[] conflictPositionQueue;
|
||||
readonly CPos?[] favoritePositions;
|
||||
|
||||
int minAssignRoleDelayTicks;
|
||||
int conflictPositionLength;
|
||||
int favoritePositionsLength;
|
||||
int currentFavoritePositionIndex;
|
||||
int alertedTicks;
|
||||
|
||||
PathFinder pathFinder;
|
||||
|
||||
public MinelayerBotModule(Actor self, MinelayerBotModuleInfo info)
|
||||
: base(info)
|
||||
{
|
||||
world = self.World;
|
||||
player = self.Owner;
|
||||
unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player;
|
||||
unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || !a.IsIdle;
|
||||
conflictPositionQueue = new CPos?[MaxPositionCacheLength];
|
||||
favoritePositions = new CPos?[MaxPositionCacheLength];
|
||||
}
|
||||
|
||||
protected override void TraitEnabled(Actor self)
|
||||
{
|
||||
// Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay.
|
||||
minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick);
|
||||
alertedTicks = 0;
|
||||
conflictPositionLength = 0;
|
||||
favoritePositionsLength = 0;
|
||||
currentFavoritePositionIndex = 0;
|
||||
pathFinder = self.World.WorldActor.Trait<PathFinder>();
|
||||
}
|
||||
|
||||
void IBotTick.BotTick(IBot bot)
|
||||
{
|
||||
if (alertedTicks > 0)
|
||||
alertedTicks--;
|
||||
|
||||
if (--minAssignRoleDelayTicks <= 0)
|
||||
{
|
||||
minAssignRoleDelayTicks = Info.ScanTick;
|
||||
|
||||
var minelayingPosition = CPos.Zero;
|
||||
var useFavoritePosition = false;
|
||||
var layMineOnHalfway = false;
|
||||
|
||||
while (conflictPositionLength > 0)
|
||||
{
|
||||
minelayingPosition = conflictPositionQueue[0].Value;
|
||||
var (hasInvalidActors, hasEnemyNearby) = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance));
|
||||
if (hasInvalidActors)
|
||||
DequeueFirstConflictPosition();
|
||||
else
|
||||
{
|
||||
layMineOnHalfway = hasEnemyNearby;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TraitPair<Minelayer>[] minelayers = null;
|
||||
|
||||
if (conflictPositionLength == 0)
|
||||
{
|
||||
// If enemy turtle themselves at base and we don't have valid position recorded,
|
||||
// we will try find a location that at the middle of pathfinding cells
|
||||
if (favoritePositionsLength == 0)
|
||||
{
|
||||
minelayers = world.ActorsWithTrait<Minelayer>().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray();
|
||||
if (minelayers.Length == 0)
|
||||
return;
|
||||
|
||||
var enemies = world.Actors.Where(IsPreferredEnemyUnit).ToArray();
|
||||
if (enemies.Length == 0)
|
||||
return;
|
||||
|
||||
var enemy = enemies.Random(world.LocalRandom);
|
||||
|
||||
foreach (var minelayer in minelayers)
|
||||
{
|
||||
var cells = pathFinder.FindPathToTargetCell(
|
||||
minelayer.Actor, [minelayer.Actor.Location], enemy.Location, BlockedByActor.Immovable, laneBias: false);
|
||||
if (cells != null && cells.Count != 0)
|
||||
{
|
||||
AIUtils.BotDebug($"{player}: try find a location to lay mine.");
|
||||
EnqueueConflictPosition(cells[cells.Count / 2]);
|
||||
|
||||
// We don't do other things in this tick, just find new location and abort
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
while (favoritePositionsLength > 0)
|
||||
{
|
||||
minelayingPosition = favoritePositions[currentFavoritePositionIndex].Value;
|
||||
var (hasInvalidActors, hasEnemyNearby) = HasInvalidActorInCircle(world.Map.CenterOfCell(minelayingPosition), WDist.FromCells(Info.AwayFromCellDistance));
|
||||
if (hasInvalidActors)
|
||||
{
|
||||
DeleteCurrentFavoritePosition();
|
||||
if (favoritePositionsLength == 0)
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
layMineOnHalfway = hasEnemyNearby;
|
||||
useFavoritePosition = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
minelayers ??= world.ActorsWithTrait<Minelayer>().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor)).ToArray();
|
||||
|
||||
if (minelayers.Length == 0)
|
||||
return;
|
||||
|
||||
var orderedActors = new List<Actor>();
|
||||
|
||||
foreach (var minelayer in minelayers)
|
||||
{
|
||||
var cells = pathFinder.FindPathToTargetCell(
|
||||
minelayer.Actor, [minelayer.Actor.Location], minelayingPosition, BlockedByActor.Immovable, laneBias: false);
|
||||
if (cells != null && cells.Count != 0)
|
||||
{
|
||||
orderedActors.Add(minelayer.Actor);
|
||||
|
||||
// if there is enemy actor nearby, we will try to lay mine on
|
||||
// 3/4 distance to desired position (the path cell is reversed)
|
||||
if (layMineOnHalfway)
|
||||
{
|
||||
minelayingPosition = cells[cells.Count * 1 / 4];
|
||||
layMineOnHalfway = false;
|
||||
}
|
||||
|
||||
if (orderedActors.Count >= Info.MaxPerAssign)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (orderedActors.Count > 0)
|
||||
{
|
||||
if (useFavoritePosition)
|
||||
{
|
||||
AIUtils.BotDebug($"{player}: Use favorite position {minelayingPosition} at index {currentFavoritePositionIndex}");
|
||||
NextFavoritePositionIndex();
|
||||
}
|
||||
else
|
||||
{
|
||||
DequeueFirstConflictPosition();
|
||||
AddPositionToFavoritePositions(minelayingPosition);
|
||||
AIUtils.BotDebug($"{player}: Use in time conflict position {minelayingPosition}");
|
||||
}
|
||||
|
||||
var vec = new CVec(Info.MineFieldRadius, Info.MineFieldRadius);
|
||||
bot.QueueOrder(
|
||||
new Order(
|
||||
"PlaceMinefield",
|
||||
null,
|
||||
Target.FromCell(world, minelayingPosition + vec),
|
||||
false,
|
||||
groupedActors: orderedActors.ToArray())
|
||||
{ ExtraLocation = minelayingPosition - vec });
|
||||
bot.QueueOrder(
|
||||
new Order(
|
||||
"Move",
|
||||
null,
|
||||
Target.FromCell(world, orderedActors[0].Location),
|
||||
true,
|
||||
groupedActors: orderedActors.ToArray()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (useFavoritePosition)
|
||||
DeleteCurrentFavoritePosition();
|
||||
else
|
||||
DequeueFirstConflictPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DequeueFirstConflictPosition()
|
||||
{
|
||||
for (var i = 1; i < conflictPositionLength; i++)
|
||||
conflictPositionQueue[i - 1] = conflictPositionQueue[i];
|
||||
|
||||
conflictPositionQueue[conflictPositionLength - 1] = null;
|
||||
conflictPositionLength--;
|
||||
}
|
||||
|
||||
void DeleteCurrentFavoritePosition()
|
||||
{
|
||||
for (var i = currentFavoritePositionIndex; i < favoritePositionsLength - 1; i++)
|
||||
favoritePositions[i] = favoritePositions[i + 1];
|
||||
favoritePositions[favoritePositionsLength - 1] = null;
|
||||
|
||||
if (--favoritePositionsLength > 0)
|
||||
currentFavoritePositionIndex %= favoritePositionsLength;
|
||||
}
|
||||
|
||||
void AddPositionToFavoritePositions(CPos cpos)
|
||||
{
|
||||
var favoriteDistSquare = Info.FavoritePositionDistance * Info.FavoritePositionDistance;
|
||||
var closestIndex = 0;
|
||||
var closestDistSquare = int.MaxValue;
|
||||
for (var i = 0; i < favoritePositionsLength; i++)
|
||||
{
|
||||
var lengthsquare = (favoritePositions[i].Value - cpos).LengthSquared;
|
||||
if (lengthsquare < closestDistSquare)
|
||||
{
|
||||
closestIndex = i;
|
||||
closestDistSquare = lengthsquare;
|
||||
}
|
||||
}
|
||||
|
||||
// Add new if there is space
|
||||
if (closestDistSquare > favoriteDistSquare && favoritePositionsLength < favoritePositions.Length)
|
||||
{
|
||||
favoritePositions[favoritePositionsLength] = cpos;
|
||||
favoritePositionsLength++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var pos = favoritePositions[closestIndex].Value;
|
||||
favoritePositions[closestIndex] = (pos - cpos) / 2 + cpos;
|
||||
}
|
||||
}
|
||||
|
||||
void NextFavoritePositionIndex()
|
||||
{
|
||||
currentFavoritePositionIndex = (currentFavoritePositionIndex + 1) % favoritePositionsLength;
|
||||
}
|
||||
|
||||
bool IsPreferredEnemyUnit(Actor actor)
|
||||
{
|
||||
if (actor == null || actor.IsDead || player.RelationshipWith(actor.Owner) != PlayerRelationship.Enemy || actor.Info.HasTraitInfo<HuskInfo>())
|
||||
return false;
|
||||
|
||||
var targetTypes = actor.GetEnabledTargetTypes();
|
||||
return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes);
|
||||
}
|
||||
|
||||
(bool HasInvalidActors, bool HasEnemyNearby) HasInvalidActorInCircle(WPos pos, WDist dist)
|
||||
{
|
||||
var hasInvalidActor = false;
|
||||
var hasEnemyActor = false;
|
||||
hasInvalidActor = world.FindActorsInCircle(pos, dist).Any(actor =>
|
||||
{
|
||||
if (actor.Owner.RelationshipWith(player) == PlayerRelationship.Ally)
|
||||
{
|
||||
var targetTypes = actor.GetEnabledTargetTypes();
|
||||
return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromAlliedTargetTypes);
|
||||
}
|
||||
|
||||
if (actor.Owner.RelationshipWith(player) == PlayerRelationship.Enemy)
|
||||
{
|
||||
hasEnemyActor = true;
|
||||
var targetTypes = actor.GetEnabledTargetTypes();
|
||||
return !targetTypes.IsEmpty && targetTypes.Overlaps(Info.AwayFromEnemyTargetTypes);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return (hasInvalidActor, hasEnemyActor);
|
||||
}
|
||||
|
||||
void EnqueueConflictPosition(CPos cell)
|
||||
{
|
||||
if (conflictPositionLength < MaxPositionCacheLength)
|
||||
{
|
||||
conflictPositionQueue[conflictPositionLength] = cell;
|
||||
conflictPositionLength++;
|
||||
}
|
||||
else
|
||||
conflictPositionQueue[MaxPositionCacheLength - 1] = cell;
|
||||
}
|
||||
|
||||
void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e)
|
||||
{
|
||||
if (alertedTicks > 0 || !IsPreferredEnemyUnit(e.Attacker))
|
||||
return;
|
||||
|
||||
alertedTicks = RepeatedAltertTicks;
|
||||
|
||||
var hasInvalidActor = HasInvalidActorInCircle(self.CenterPosition, WDist.FromCells(Info.AwayFromCellDistance)).HasInvalidActors;
|
||||
if (hasInvalidActor)
|
||||
return;
|
||||
|
||||
var targetTypes = self.GetEnabledTargetTypes();
|
||||
CPos pos;
|
||||
if (!targetTypes.IsEmpty && targetTypes.Overlaps(Info.UseEnemyLocationTargetTypes))
|
||||
pos = e.Attacker.Location;
|
||||
else
|
||||
pos = self.Location;
|
||||
|
||||
EnqueueConflictPosition(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
#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;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
[Desc("Adds metadata for the AI bots.")]
|
||||
public class SupportPowerDecision
|
||||
{
|
||||
[Desc("What is the minimum attractiveness we will use this power for?")]
|
||||
public readonly int MinimumAttractiveness = 1;
|
||||
|
||||
[Desc("What support power does this decision apply to?")]
|
||||
public readonly string OrderName = "AirstrikePowerInfoOrder";
|
||||
|
||||
[Desc(
|
||||
"What is the coarse scan radius of this power?",
|
||||
"For finding the general target area, before doing a detail scan",
|
||||
"Should be 10 or more to avoid lag")]
|
||||
public readonly int CoarseScanRadius = 20;
|
||||
|
||||
[Desc(
|
||||
"What is the fine scan radius of this power?",
|
||||
"For doing a detailed scan in the general target area.",
|
||||
"Minimum is 1")]
|
||||
public readonly int FineScanRadius = 2;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadConsiderations))]
|
||||
[Desc("The decisions associated with this power")]
|
||||
public readonly ImmutableArray<Consideration> Considerations = [];
|
||||
|
||||
[Desc("Minimum ticks to wait until next Decision scan attempt.")]
|
||||
public readonly int MinimumScanTimeInterval = 250;
|
||||
|
||||
[Desc("Maximum ticks to wait until next Decision scan attempt.")]
|
||||
public readonly int MaximumScanTimeInterval = 262;
|
||||
|
||||
public SupportPowerDecision(MiniYaml yaml)
|
||||
{
|
||||
FieldLoader.Load(this, yaml);
|
||||
}
|
||||
|
||||
static object LoadConsiderations(MiniYaml yaml)
|
||||
{
|
||||
var ret = new List<Consideration>();
|
||||
foreach (var d in yaml.Nodes)
|
||||
if (d.Key.Split('@')[0] == "Consideration")
|
||||
ret.Add(new Consideration(d.Value));
|
||||
|
||||
return ret.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>Evaluates the attractiveness of a position according to all considerations.</summary>
|
||||
public int GetAttractiveness(WPos pos, Player firedBy)
|
||||
{
|
||||
var answer = 0;
|
||||
var world = firedBy.World;
|
||||
var targetTile = world.Map.CellContaining(pos);
|
||||
|
||||
if (!world.Map.Contains(targetTile))
|
||||
return 0;
|
||||
|
||||
foreach (var consideration in Considerations)
|
||||
{
|
||||
var radiusToUse = new WDist(consideration.CheckRadius.Length);
|
||||
|
||||
var checkActors = world.FindActorsInCircle(pos, radiusToUse);
|
||||
foreach (var scrutinized in checkActors)
|
||||
answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner), firedBy);
|
||||
|
||||
var delta = new WVec(radiusToUse, radiusToUse, WDist.Zero);
|
||||
var tl = world.Map.CellContaining(pos - delta);
|
||||
var br = world.Map.CellContaining(pos + delta);
|
||||
var checkFrozen = firedBy.FrozenActorLayer.FrozenActorsInRegion(new CellRegion(world.Map.Grid.Type, tl, br));
|
||||
|
||||
// IsValid check filters out Frozen Actors that have not initialized their Owner
|
||||
foreach (var scrutinized in checkFrozen)
|
||||
answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner));
|
||||
}
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
/// <summary>Evaluates the attractiveness of a group of actors according to all considerations.</summary>
|
||||
public int GetAttractiveness(IEnumerable<Actor> actors, Player firedBy)
|
||||
{
|
||||
var answer = 0;
|
||||
|
||||
foreach (var consideration in Considerations)
|
||||
foreach (var scrutinized in actors)
|
||||
answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner), firedBy);
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
public int GetAttractiveness(IEnumerable<FrozenActor> frozenActors, Player firedBy)
|
||||
{
|
||||
var answer = 0;
|
||||
|
||||
foreach (var consideration in Considerations)
|
||||
foreach (var scrutinized in frozenActors)
|
||||
if (scrutinized.IsValid && scrutinized.Visible)
|
||||
answer += consideration.GetAttractiveness(scrutinized, firedBy.RelationshipWith(scrutinized.Owner));
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
public int GetNextScanTime(World world) { return world.LocalRandom.Next(MinimumScanTimeInterval, MaximumScanTimeInterval); }
|
||||
|
||||
/// <summary>Makes up part of a decision, describing how to evaluate a target.</summary>
|
||||
public class Consideration
|
||||
{
|
||||
public enum DecisionMetric { Health, Value, None }
|
||||
|
||||
[Desc("Against whom should this power be used?", "Allowed keywords: Ally, Neutral, Enemy")]
|
||||
public readonly PlayerRelationship Against = PlayerRelationship.Enemy;
|
||||
|
||||
[Desc("What types should the desired targets of this power be?")]
|
||||
public readonly BitSet<TargetableType> Types = new("Air", "Ground", "Water");
|
||||
|
||||
[Desc("How attractive are these types of targets?")]
|
||||
public readonly int Attractiveness = 100;
|
||||
|
||||
[Desc("Weight the target attractiveness by this property", "Allowed keywords: Health, Value, None")]
|
||||
public readonly DecisionMetric TargetMetric = DecisionMetric.None;
|
||||
|
||||
[Desc("What is the check radius of this decision?")]
|
||||
public readonly WDist CheckRadius = WDist.FromCells(5);
|
||||
|
||||
public Consideration(MiniYaml yaml)
|
||||
{
|
||||
FieldLoader.Load(this, yaml);
|
||||
}
|
||||
|
||||
/// <summary>Evaluates a single actor according to the rules defined in this consideration.</summary>
|
||||
public int GetAttractiveness(Actor a, PlayerRelationship stance, Player firedBy)
|
||||
{
|
||||
if (stance != Against)
|
||||
return 0;
|
||||
|
||||
if (a == null)
|
||||
return 0;
|
||||
|
||||
if (!a.IsTargetableBy(firedBy.PlayerActor) || !a.CanBeViewedByPlayer(firedBy))
|
||||
return 0;
|
||||
|
||||
if (Types.Overlaps(a.GetEnabledTargetTypes()))
|
||||
{
|
||||
switch (TargetMetric)
|
||||
{
|
||||
case DecisionMetric.Value:
|
||||
var valueInfo = a.Info.TraitInfoOrDefault<ValuedInfo>();
|
||||
return (valueInfo != null) ? valueInfo.Cost * Attractiveness : 0;
|
||||
|
||||
case DecisionMetric.Health:
|
||||
var health = a.TraitOrDefault<IHealth>();
|
||||
|
||||
if (health == null)
|
||||
return 0;
|
||||
|
||||
// Cast to long to avoid overflow when multiplying by the health
|
||||
return (int)((long)health.HP * Attractiveness / health.MaxHP);
|
||||
|
||||
default:
|
||||
return Attractiveness;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetAttractiveness(FrozenActor fa, PlayerRelationship stance)
|
||||
{
|
||||
if (stance != Against)
|
||||
return 0;
|
||||
|
||||
if (fa == null || !fa.IsValid || !fa.Visible)
|
||||
return 0;
|
||||
|
||||
if (Types.Overlaps(fa.TargetTypes))
|
||||
{
|
||||
switch (TargetMetric)
|
||||
{
|
||||
case DecisionMetric.Value:
|
||||
var valueInfo = fa.Info.TraitInfoOrDefault<ValuedInfo>();
|
||||
return (valueInfo != null) ? valueInfo.Cost * Attractiveness : 0;
|
||||
|
||||
case DecisionMetric.Health:
|
||||
var healthInfo = fa.Info.TraitInfoOrDefault<IHealthInfo>();
|
||||
return (healthInfo != null) ? fa.HP * Attractiveness / healthInfo.MaxHP : 0;
|
||||
|
||||
default:
|
||||
return Attractiveness;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user