Skip to content

Commit 0c7a537

Browse files
committed
Merge remote-tracking branch 'darkademic/dev'
2 parents cb0ab1d + 2426a18 commit 0c7a537

60 files changed

Lines changed: 2343 additions & 1090 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

OpenRA.Mods.CA/AIUtils.cs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
*/
99
#endregion
1010

11+
using System;
1112
using System.Collections.Generic;
1213
using System.Linq;
1314
using OpenRA.Mods.Common;
15+
using OpenRA.Mods.Common.Pathfinder;
1416
using OpenRA.Mods.Common.Traits;
1517
using OpenRA.Traits;
1618

@@ -114,5 +116,213 @@ public static WPos PositionClosestTo_Old(this IEnumerable<WPos> positions, WPos
114116
{
115117
return positions.MinByOrDefault(p => (p - pos).LengthSquared);
116118
}
119+
120+
// Finds multiple distinct routes between source and target for a given locomotor.
121+
public static List<List<CPos>> FindDistinctRoutes(
122+
World world,
123+
Locomotor locomotor,
124+
CPos source,
125+
CPos target,
126+
int maxRoutes = 3,
127+
BlockedByActor check = BlockedByActor.None)
128+
{
129+
var routes = new List<List<CPos>>();
130+
131+
// Get the PathFinder trait from the world actor
132+
var pathFinder = world.WorldActor.TraitOrDefault<PathFinder>();
133+
if (pathFinder == null)
134+
return routes;
135+
136+
// Get the abstract graph data from the hierarchical pathfinder
137+
var (abstractGraph, abstractDomains) = pathFinder.GetOverlayDataForLocomotor(locomotor, check);
138+
if (abstractGraph == null || abstractDomains == null)
139+
return routes;
140+
141+
// Map source and target to their abstract nodes
142+
var sourceAbstract = FindAbstractNodeForCell(source, abstractGraph, abstractDomains);
143+
var targetAbstract = FindAbstractNodeForCell(target, abstractGraph, abstractDomains);
144+
145+
if (sourceAbstract == null || targetAbstract == null)
146+
return routes;
147+
148+
// Check if source and target are in the same domain (connected)
149+
if (!abstractDomains.TryGetValue(sourceAbstract.Value, out var sourceDomain) ||
150+
!abstractDomains.TryGetValue(targetAbstract.Value, out var targetDomain) ||
151+
sourceDomain != targetDomain)
152+
return routes;
153+
154+
// Track nodes that have been used in previous routes
155+
var excludedNodes = new HashSet<CPos>();
156+
157+
for (var i = 0; i < maxRoutes; i++)
158+
{
159+
var route = FindAbstractPath(sourceAbstract.Value, targetAbstract.Value, abstractGraph, excludedNodes);
160+
if (route == null || route.Count == 0)
161+
break; // No more valid routes available
162+
163+
routes.Add(route);
164+
165+
// Exclude all nodes in this route (except source and target) for future routes
166+
foreach (var node in route)
167+
{
168+
if (node != sourceAbstract.Value
169+
&& node != targetAbstract.Value
170+
&& node != route.ElementAtOrDefault(1)
171+
&& node != route.ElementAtOrDefault(2)
172+
&& node != route.ElementAtOrDefault(route.Count - 2))
173+
excludedNodes.Add(node);
174+
}
175+
}
176+
177+
return routes;
178+
}
179+
180+
// Finds the abstract node that corresponds to a given cell position.
181+
static CPos? FindAbstractNodeForCell(
182+
CPos cell,
183+
IReadOnlyDictionary<CPos, List<GraphConnection>> abstractGraph,
184+
IReadOnlyDictionary<CPos, uint> abstractDomains)
185+
{
186+
// If the cell itself is an abstract node, return it
187+
if (abstractDomains.ContainsKey(cell))
188+
return cell;
189+
190+
// Otherwise, find the nearest abstract node
191+
CPos? nearestNode = null;
192+
var minDistSq = int.MaxValue;
193+
194+
foreach (var abstractNode in abstractDomains.Keys)
195+
{
196+
var distSq = (abstractNode - cell).LengthSquared;
197+
if (distSq < minDistSq)
198+
{
199+
minDistSq = distSq;
200+
nearestNode = abstractNode;
201+
}
202+
}
203+
204+
return nearestNode;
205+
}
206+
207+
// Performs A* pathfinding on the abstract graph to find a route from source to target,
208+
// avoiding any nodes in the excluded set.
209+
static List<CPos> FindAbstractPath(
210+
CPos source,
211+
CPos target,
212+
IReadOnlyDictionary<CPos, List<GraphConnection>> abstractGraph,
213+
HashSet<CPos> excludedNodes)
214+
{
215+
var openSet = new Dictionary<CPos, PathNode>();
216+
var closedSet = new HashSet<CPos>();
217+
218+
// Initialize the starting node
219+
var startNode = new PathNode
220+
{
221+
Position = source,
222+
CostFromStart = 0,
223+
EstimatedTotalCost = Heuristic(source, target),
224+
Parent = null
225+
};
226+
227+
openSet[source] = startNode;
228+
229+
while (openSet.Count > 0)
230+
{
231+
// Find the node in openSet with the lowest estimated total cost
232+
var current = openSet.Values.MinBy(n => n.EstimatedTotalCost);
233+
if (current == null)
234+
break;
235+
236+
// Check if we've reached the target
237+
if (current.Position == target)
238+
return ReconstructPath(current);
239+
240+
openSet.Remove(current.Position);
241+
closedSet.Add(current.Position);
242+
243+
// Explore neighbors
244+
if (!abstractGraph.TryGetValue(current.Position, out var connections))
245+
continue;
246+
247+
foreach (var connection in connections)
248+
{
249+
var neighbor = connection.Destination;
250+
251+
// Skip if already evaluated or excluded (unless it's the target)
252+
if (closedSet.Contains(neighbor) || (excludedNodes.Contains(neighbor) && neighbor != target))
253+
continue;
254+
255+
var newCost = current.CostFromStart + connection.Cost;
256+
257+
// If this is a new node or we found a better path to it
258+
if (!openSet.TryGetValue(neighbor, out var neighborNode))
259+
{
260+
neighborNode = new PathNode
261+
{
262+
Position = neighbor,
263+
CostFromStart = newCost,
264+
EstimatedTotalCost = newCost + Heuristic(neighbor, target),
265+
Parent = current
266+
};
267+
openSet[neighbor] = neighborNode;
268+
}
269+
else if (newCost < neighborNode.CostFromStart)
270+
{
271+
neighborNode.CostFromStart = newCost;
272+
neighborNode.EstimatedTotalCost = newCost + Heuristic(neighbor, target);
273+
neighborNode.Parent = current;
274+
}
275+
}
276+
}
277+
278+
// No path found
279+
return null;
280+
}
281+
282+
// Reconstructs the path by following parent pointers from target back to source.
283+
static List<CPos> ReconstructPath(PathNode targetNode)
284+
{
285+
var path = new List<CPos>();
286+
var current = targetNode;
287+
288+
while (current != null)
289+
{
290+
path.Add(current.Position);
291+
current = current.Parent;
292+
}
293+
294+
path.Reverse();
295+
return path;
296+
}
297+
298+
// Simple heuristic function for A* pathfinding (Manhattan distance).
299+
static int Heuristic(CPos from, CPos to)
300+
{
301+
var delta = from - to;
302+
return Math.Abs(delta.X) + Math.Abs(delta.Y);
303+
}
304+
305+
// Helper class to represent a node in the A* pathfinding algorithm.
306+
class PathNode
307+
{
308+
public CPos Position;
309+
public int CostFromStart;
310+
public int EstimatedTotalCost;
311+
public PathNode Parent;
312+
}
313+
314+
public static bool PathExistsForLocomotor(
315+
World world,
316+
Locomotor locomotor,
317+
CPos source,
318+
CPos target)
319+
{
320+
var pathFinder = world.WorldActor.TraitOrDefault<IPathFinder>();
321+
322+
if (pathFinder == null)
323+
return false;
324+
325+
return pathFinder.PathExistsForLocomotor(locomotor, source, target);
326+
}
117327
}
118328
}

OpenRA.Mods.CA/Scripting/BuildingProperties.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ public BuildingProperties(ScriptContext context, Actor self)
2727
}
2828

2929
[Desc("Returns the building's footprint cells.")]
30-
public CPos[] FootprintCells => building.Info.Tiles(Self.Location).ToArray();
30+
public CPos[] FootprintCells => building.Info.Tiles(Self.Location).OrderBy(t => t.Y).ThenBy(t => t.X).ToArray();
3131
}
3232
}

OpenRA.Mods.CA/Scripting/TargetedLeapAbilityGlobal.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public TargetableLeapAbilityProperties(ScriptContext context, Actor self)
3030
public void TargetedLeap(CPos targetCell)
3131
{
3232
var target = Target.FromCell(Self.World, targetCell);
33-
LeapAbility.ResolveOrder(Self, new Order("TargetableLeapOrderLeap", Self, target, true));
33+
LeapAbility.ResolveOrder(Self, new Order("TargetedLeapOrderLeap", Self, target, true));
3434
}
3535
}
3636
}

OpenRA.Mods.CA/Traits/BotModules/SquadManagerBotModuleCA.cs

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public class SquadManagerBotModuleCAInfo : ConditionalTraitInfo
3030
[Desc("Actor types that are excluded from ground attacks.")]
3131
public readonly HashSet<string> AirUnitsTypes = new HashSet<string>();
3232

33+
[ActorReference]
34+
[Desc("Actor types that are valid for harasser squads.")]
35+
public readonly HashSet<string> HarasserTypes = new HashSet<string>();
36+
3337
[ActorReference]
3438
[Desc("Actor types that should generally be excluded from attack squads.")]
3539
public readonly HashSet<string> ExcludeFromSquadsTypes = new HashSet<string>();
@@ -49,12 +53,6 @@ public class SquadManagerBotModuleCAInfo : ConditionalTraitInfo
4953
[Desc("Target types are used for identifying aircraft.")]
5054
public readonly BitSet<TargetableType> AircraftTargetType = new("Air");
5155

52-
[Desc("Minimum number of units AI must have before attacking.")]
53-
public readonly int SquadSize = 8;
54-
55-
[Desc("Random number of up to this many units is added to squad size when creating an attack squad.")]
56-
public readonly int SquadSizeRandomBonus = 30;
57-
5856
[Desc("Delay (in ticks) between giving out orders to units.")]
5957
public readonly int AssignRolesInterval = 50;
6058

@@ -102,8 +100,14 @@ public class SquadManagerBotModuleCAInfo : ConditionalTraitInfo
102100
[Desc("Minimum value of units AI must have before attacking.")]
103101
public readonly int SquadValue = 0;
104102

105-
[Desc("Random number of up to this value units is added to squad valuee when creating an attack squad.")]
106-
public readonly int SquadValueRandomBonus = 0;
103+
[Desc("Random number of up to this value units is added to squad value when creating an attack squad.")]
104+
public readonly int SquadValueMaxEarlyBonus = 0;
105+
106+
[Desc("The random number added to squad value increases to this value over the first 20 minutes.")]
107+
public readonly int SquadValueMinLateBonus = 0;
108+
109+
[Desc("The random number added to squad value increases to this value over the first 20 minutes.")]
110+
public readonly int SquadValueMaxLateBonus = 0;
107111

108112
[Desc("Percent change for ground squads to attack a random priority target rather than the closest enemy.")]
109113
public readonly int HighValueTargetPriority = 0;
@@ -123,12 +127,21 @@ public class SquadManagerBotModuleCAInfo : ConditionalTraitInfo
123127
[Desc("Air threats to prioritise above all others.")]
124128
public readonly HashSet<string> BigAirThreats = new HashSet<string>();
125129

130+
[Desc("Percent chance to take a less direct route to targets.")]
131+
public readonly int IndirectRouteChance = 50;
132+
126133
public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
127134
{
128135
base.RulesetLoaded(rules, ai);
129136

130137
if (DangerScanRadius <= 0)
131138
throw new YamlException("DangerScanRadius must be greater than zero.");
139+
140+
if (SquadValueMaxEarlyBonus > SquadValueMaxLateBonus)
141+
throw new YamlException("SquadValueMaxEarlyBonus cannot be greater than SquadValueMaxLateBonus.");
142+
143+
if (SquadValueMinLateBonus > SquadValueMaxLateBonus)
144+
throw new YamlException("SquadValueMinLateBonus cannot be greater than SquadValueMaxLateBonus.");
132145
}
133146

134147
public override object Create(ActorInitializer init) { return new SquadManagerBotModuleCA(init.Self, this); }
@@ -175,7 +188,8 @@ public CPos GetRandomBaseCenter()
175188
Actor protectOwnFrom; // CA: Track what we're protecting from
176189

177190
int desiredAttackForceValue; // CA: Value-based squad thresholds
178-
int desiredAttackForceSize;
191+
192+
Dictionary<string, int> cachedUnitValues = new();
179193

180194
public SquadManagerBotModuleCA(Actor self, SquadManagerBotModuleCAInfo info)
181195
: base(info)
@@ -482,6 +496,27 @@ void FindNewUnits(IBot bot)
482496
newNavalSquad.Units.Add(a);
483497
}
484498
}
499+
else if (Info.HarasserTypes.Contains(a.Info.Name))
500+
{
501+
var harasserSquads = Squads.Where(s => s.Type == SquadCAType.Harass);
502+
var matchingHarasserSquadFound = false;
503+
504+
foreach (var harasserSquad in harasserSquads)
505+
{
506+
if (harasserSquad.Units.Any(u => u.Info.Name == a.Info.Name))
507+
{
508+
harasserSquad.Units.Add(a);
509+
matchingHarasserSquadFound = true;
510+
break;
511+
}
512+
}
513+
514+
if (!matchingHarasserSquadFound)
515+
{
516+
var newHarasserSquad = RegisterNewSquad(bot, SquadCAType.Harass);
517+
newHarasserSquad.Units.Add(a);
518+
}
519+
}
485520
else
486521
unitsHangingAroundTheBase.Add(a);
487522

@@ -503,13 +538,17 @@ void CreateAttackForce(IBot bot)
503538
{
504539
foreach (var a in unitsHangingAroundTheBase)
505540
{
506-
var valued = a.Info.TraitInfoOrDefault<ValuedInfo>();
507-
if (valued != null)
508-
idleUnitsValue += valued.Cost;
541+
if (!cachedUnitValues.TryGetValue(a.Info.Name, out var unitCost))
542+
{
543+
unitCost = a.Info.TraitInfoOrDefault<ValuedInfo>()?.Cost ?? 0;
544+
cachedUnitValues[a.Info.Name] = unitCost;
545+
}
546+
547+
idleUnitsValue += unitCost;
509548
}
510549
}
511550

512-
if (idleUnitsValue >= desiredAttackForceValue && unitsHangingAroundTheBase.Count >= desiredAttackForceSize)
551+
if (idleUnitsValue >= desiredAttackForceValue)
513552
{
514553
var attackForce = RegisterNewSquad(bot, SquadCAType.Assault);
515554

@@ -526,11 +565,18 @@ void CreateAttackForce(IBot bot)
526565

527566
void SetNextDesiredAttackForce()
528567
{
529-
desiredAttackForceSize = Info.SquadSize + World.LocalRandom.Next(Info.SquadSizeRandomBonus);
530-
desiredAttackForceValue = 0;
568+
desiredAttackForceValue = Info.SquadValue;
569+
570+
// Add a random bonus between a min and max that scale over the first 20 minutes.
571+
// Min scales from 0 to SquadValueMinLateBonus; max scales from SquadValueMaxEarlyBonus to SquadValueMaxLateBonus.
572+
var t = Math.Min(1f, World.WorldTick / (20f * 60f * 25f));
573+
var minBonus = (int)(Info.SquadValueMinLateBonus * t);
574+
var maxBonus = (int)(Info.SquadValueMaxEarlyBonus + (Info.SquadValueMaxLateBonus - Info.SquadValueMaxEarlyBonus) * t);
531575

532-
if (Info.SquadValue > 0) // CA: Value-based squad thresholds
533-
desiredAttackForceValue = Info.SquadValue + World.LocalRandom.Next(Info.SquadValueRandomBonus);
576+
if (maxBonus <= minBonus)
577+
desiredAttackForceValue += minBonus;
578+
else
579+
desiredAttackForceValue += World.LocalRandom.Next(minBonus, maxBonus);
534580
}
535581

536582
void ProtectOwn(IBot bot, Actor attacker)
@@ -671,7 +717,7 @@ bool IsValidEnemyUnit(Actor a)
671717

672718
public bool IsPreferredEnemyBuilding(Actor a)
673719
{
674-
return IsValidEnemyUnit(a) && a.Info.HasTraitInfo<BuildingInfo>();
720+
return IsValidEnemyUnit(a) && a.Info.HasTraitInfo<RepairableBuildingInfo>();
675721
}
676722

677723
public bool IsPreferredEnemyAircraft(Actor a)

0 commit comments

Comments
 (0)