Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

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:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
#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 AI.Fuzzy.Library;
using OpenRA.Mods.Common.Warheads;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
sealed class AttackOrFleeFuzzy
{
static readonly string[] DefaultRulesNormalOwnHealth =
[
"if ((OwnHealth is Normal) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Attack"
];
static readonly string[] DefaultRulesInjuredOwnHealth =
[
"if ((OwnHealth is Injured) " +
"and (EnemyHealth is NearDead) " +
"and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Attack",
"if ((OwnHealth is Injured) " +
"and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and ((RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Attack",
"if ((OwnHealth is Injured) " +
"and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and (RelativeAttackPower is Weak) " +
"and (RelativeSpeed is Slow)) " +
"then AttackOrFlee is Attack",
"if ((OwnHealth is Injured) " +
"and ((EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and (RelativeAttackPower is Weak) " +
"and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Flee",
"if ((OwnHealth is Injured) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and (RelativeSpeed is Slow)) " +
"then AttackOrFlee is Attack"
];
static readonly string[] DefaultRulesNearDeadOwnHealth =
[
"if ((OwnHealth is NearDead) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured)) " +
"and ((RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal))) " +
"then AttackOrFlee is Attack",
"if ((OwnHealth is NearDead) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured)) " +
"and (RelativeAttackPower is Weak) " +
"and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Flee",
"if ((OwnHealth is NearDead) " +
"and (EnemyHealth is Normal) " +
"and (RelativeAttackPower is Weak) " +
"and ((RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Flee",
"if (OwnHealth is NearDead) " +
"and (EnemyHealth is Normal) " +
"and ((RelativeAttackPower is Equal) or (RelativeAttackPower is Strong)) " +
"and (RelativeSpeed is Fast) " +
"then AttackOrFlee is Flee",
"if (OwnHealth is NearDead) " +
"and (EnemyHealth is Injured) " +
"and (RelativeAttackPower is Equal) " +
"and (RelativeSpeed is Fast) " +
"then AttackOrFlee is Flee"
];
public static readonly AttackOrFleeFuzzy Default = new(null, null, null);
public static readonly AttackOrFleeFuzzy Rush = new(
[
"if ((OwnHealth is Normal) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and (RelativeAttackPower is Strong) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Attack",
"if ((OwnHealth is Normal) " +
"and ((EnemyHealth is NearDead) or (EnemyHealth is Injured) or (EnemyHealth is Normal)) " +
"and ((RelativeAttackPower is Weak) or (RelativeAttackPower is Equal)) " +
"and ((RelativeSpeed is Slow) or (RelativeSpeed is Equal) or (RelativeSpeed is Fast))) " +
"then AttackOrFlee is Flee"
], null, null);
readonly MamdaniFuzzySystem fuzzyEngine = new();
public AttackOrFleeFuzzy(
IEnumerable<string> rulesForNormalOwnHealth,
IEnumerable<string> rulesForInjuredOwnHealth,
IEnumerable<string> rulesForNeadDeadOwnHealth)
{
lock (fuzzyEngine)
{
var playerHealthFuzzy = new FuzzyVariable("OwnHealth", 0.0, 100.0);
playerHealthFuzzy.Terms.Add(new FuzzyTerm("NearDead", new TrapezoidMembershipFunction(0, 0, 20, 40)));
playerHealthFuzzy.Terms.Add(new FuzzyTerm("Injured", new TrapezoidMembershipFunction(30, 50, 50, 70)));
playerHealthFuzzy.Terms.Add(new FuzzyTerm("Normal", new TrapezoidMembershipFunction(50, 80, 100, 100)));
fuzzyEngine.Input.Add(playerHealthFuzzy);
var enemyHealthFuzzy = new FuzzyVariable("EnemyHealth", 0.0, 100.0);
enemyHealthFuzzy.Terms.Add(new FuzzyTerm("NearDead", new TrapezoidMembershipFunction(0, 0, 20, 40)));
enemyHealthFuzzy.Terms.Add(new FuzzyTerm("Injured", new TrapezoidMembershipFunction(30, 50, 50, 70)));
enemyHealthFuzzy.Terms.Add(new FuzzyTerm("Normal", new TrapezoidMembershipFunction(50, 80, 100, 100)));
fuzzyEngine.Input.Add(enemyHealthFuzzy);
var relativeAttackPowerFuzzy = new FuzzyVariable("RelativeAttackPower", 0.0, 1000.0);
relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Weak", new TrapezoidMembershipFunction(0, 0, 70, 90)));
relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Equal", new TrapezoidMembershipFunction(85, 100, 100, 115)));
relativeAttackPowerFuzzy.Terms.Add(new FuzzyTerm("Strong", new TrapezoidMembershipFunction(110, 150, 150, 1000)));
fuzzyEngine.Input.Add(relativeAttackPowerFuzzy);
var relativeSpeedFuzzy = new FuzzyVariable("RelativeSpeed", 0.0, 1000.0);
relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Slow", new TrapezoidMembershipFunction(0, 0, 70, 90)));
relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Equal", new TrapezoidMembershipFunction(85, 100, 100, 115)));
relativeSpeedFuzzy.Terms.Add(new FuzzyTerm("Fast", new TrapezoidMembershipFunction(110, 150, 150, 1000)));
fuzzyEngine.Input.Add(relativeSpeedFuzzy);
var attackOrFleeFuzzy = new FuzzyVariable("AttackOrFlee", 0.0, 50.0);
attackOrFleeFuzzy.Terms.Add(new FuzzyTerm("Attack", new TrapezoidMembershipFunction(0, 15, 15, 30)));
attackOrFleeFuzzy.Terms.Add(new FuzzyTerm("Flee", new TrapezoidMembershipFunction(25, 35, 35, 50)));
fuzzyEngine.Output.Add(attackOrFleeFuzzy);
foreach (var rule in rulesForNormalOwnHealth ?? DefaultRulesNormalOwnHealth)
AddFuzzyRule(rule);
foreach (var rule in rulesForInjuredOwnHealth ?? DefaultRulesInjuredOwnHealth)
AddFuzzyRule(rule);
foreach (var rule in rulesForNeadDeadOwnHealth ?? DefaultRulesNearDeadOwnHealth)
AddFuzzyRule(rule);
}
}
void AddFuzzyRule(string rule)
{
fuzzyEngine.Rules.Add(fuzzyEngine.ParseRule(rule));
}
public bool CanAttack(IReadOnlyCollection<Actor> ownUnits, IReadOnlyCollection<Actor> enemyUnits)
{
double attackChance;
var inputValues = new Dictionary<FuzzyVariable, double>();
lock (fuzzyEngine)
{
inputValues.Add(fuzzyEngine.InputByName("OwnHealth"), NormalizedHealth(ownUnits, 100));
inputValues.Add(fuzzyEngine.InputByName("EnemyHealth"), NormalizedHealth(enemyUnits, 100));
inputValues.Add(fuzzyEngine.InputByName("RelativeAttackPower"), RelativePower(ownUnits, enemyUnits));
inputValues.Add(fuzzyEngine.InputByName("RelativeSpeed"), RelativeSpeed(ownUnits, enemyUnits));
var result = fuzzyEngine.Calculate(inputValues);
attackChance = result[fuzzyEngine.OutputByName("AttackOrFlee")];
}
return !double.IsNaN(attackChance) && attackChance < 30.0;
}
static float NormalizedHealth(IEnumerable<Actor> actors, int normalizeByValue)
{
var sumOfMaxHp = 0;
var sumOfHp = 0;
foreach (var a in actors)
{
if (a.Info.HasTraitInfo<IHealthInfo>())
{
sumOfMaxHp += a.Trait<IHealth>().MaxHP;
sumOfHp += a.Trait<IHealth>().HP;
}
}
if (sumOfMaxHp == 0)
return 0.0f;
// Cast to long to avoid overflow when multiplying by the health
return (int)((long)sumOfHp * normalizeByValue / sumOfMaxHp);
}
static float RelativePower(IReadOnlyCollection<Actor> own, IReadOnlyCollection<Actor> enemy)
{
return RelativeValue(own, enemy, 100, SumOfValues<AttackBaseInfo>, a =>
{
var sumOfDamage = 0;
var arms = a.TraitsImplementing<Armament>();
foreach (var arm in arms)
{
var burst = arm.Weapon.Burst;
// For simplicity's sake, we're only factoring in the first burst delay, as more than one burst delay is extremely rare.
// Additionally, clamping total delay to minimum of 1 (ReloadDelay: 0 is technically possible) and maximum of 200.
// High dmg/low ROF weapons shouldn't be rated too low as high dmg/shot can outweigh mere dps due to likelier 1-hit-kills.
// TODO: Revisit this at some point to replace the arbitrary cap with something smarter.
var totalReloadDelay = arm.Weapon.ReloadDelay + (arm.Weapon.BurstDelays[0] * (burst - 1)).Clamp(1, 200);
var damageWarheads = arm.Weapon.Warheads.OfType<DamageWarhead>();
foreach (var warhead in damageWarheads)
sumOfDamage += warhead.Damage * burst / totalReloadDelay * 100;
}
return sumOfDamage;
});
}
static float RelativeSpeed(IReadOnlyCollection<Actor> own, IReadOnlyCollection<Actor> enemy)
{
return RelativeValue(own, enemy, 100, Average<MobileInfo>, a => a.Info.TraitInfo<MobileInfo>().Speed);
}
static float RelativeValue(IReadOnlyCollection<Actor> own, IReadOnlyCollection<Actor> enemy, float normalizeByValue,
Func<IReadOnlyCollection<Actor>, Func<Actor, int>, float> relativeFunc, Func<Actor, int> getValue)
{
if (enemy.Count == 0)
return 999.0f;
if (own.Count == 0)
return 0.0f;
var relative = relativeFunc(own, getValue) / relativeFunc(enemy, getValue) * normalizeByValue;
return relative.Clamp(0.0f, 999.0f);
}
static float SumOfValues<TTraitInfo>(IEnumerable<Actor> actors, Func<Actor, int> getValue) where TTraitInfo : ITraitInfoInterface
{
var sum = 0;
foreach (var a in actors)
if (a.Info.HasTraitInfo<TTraitInfo>())
sum += getValue(a);
return sum;
}
static float Average<TTraitInfo>(IEnumerable<Actor> actors, Func<Actor, int> getValue) where TTraitInfo : ITraitInfoInterface
{
var sum = 0;
var countActors = 0;
foreach (var a in actors)
{
if (a.Info.HasTraitInfo<TTraitInfo>())
{
sum += getValue(a);
countActors++;
}
}
if (countActors == 0)
return 0.0f;
return sum / countActors;
}
}
}

View File

@@ -0,0 +1,182 @@
#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.Linq;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
public enum SquadType { Assault, Air, Rush, Protection, Naval }
public class Squad
{
public HashSet<Actor> Units = [];
public SquadType Type;
internal IBot Bot;
internal World World;
internal SquadManagerBotModule SquadManager;
internal MersenneTwister Random;
internal StateMachine FuzzyStateMachine;
/// <summary>
/// Target location to attack. This will be either the targeted actor,
/// or a position close to that actor sufficient to get within weapons range.
/// </summary>
internal Target Target { get; set; }
/// <summary>
/// Actor that is targeted, for any actor based checks. Use <see cref="Target"/> for a targeting location.
/// </summary>
internal Actor TargetActor;
public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type)
: this(bot, squadManager, type, default) { }
public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, (Actor Actor, WVec Offset) target)
{
Bot = bot;
SquadManager = squadManager;
World = bot.Player.PlayerActor.World;
Random = World.LocalRandom;
Type = type;
SetActorToTarget(target);
FuzzyStateMachine = new StateMachine();
switch (type)
{
case SquadType.Assault:
case SquadType.Rush:
case SquadType.Naval:
FuzzyStateMachine.ChangeState(this, new GroundUnitsIdleState());
break;
case SquadType.Air:
FuzzyStateMachine.ChangeState(this, new AirIdleState());
break;
case SquadType.Protection:
FuzzyStateMachine.ChangeState(this, new UnitsForProtectionIdleState());
break;
}
}
public void Update()
{
if (IsValid)
FuzzyStateMachine.Update(this);
}
public bool IsValid => Units.Count > 0;
public void SetActorToTarget((Actor Actor, WVec Offset) target)
{
TargetActor = target.Actor;
if (TargetActor == null)
{
Target = Target.Invalid;
return;
}
if (target.Offset == WVec.Zero)
Target = Target.FromActor(TargetActor);
else
Target = Target.FromPos(TargetActor.CenterPosition + target.Offset);
}
/// <summary>
/// Checks the target is still valid, and updates the <see cref="Target"/> location if it is still valid.
/// </summary>
public bool IsTargetValid(Actor squadUnit)
{
var valid =
TargetActor != null &&
TargetActor.IsInWorld &&
!TargetActor.IsDead &&
Units.Any(Target.IsValidFor) &&
!TargetActor.Info.HasTraitInfo<HuskInfo>();
if (!valid)
return false;
// Refresh the target location.
// If the actor moved out of reach then we'll mark it invalid.
// e.g. a ship targeting a land unit that moves inland out of weapons range.
// or the target crossed a bridge which is then destroyed.
// If it is still in range but we have to target a nearby location, we can update that location.
// e.g. a ship targeting a land unit, but the land unit moved north.
// We need to update our location to move north as well.
// If we can reach the actor directly, we'll just target it directly.
var target = SquadManager.FindEnemies([TargetActor], squadUnit).FirstOrDefault();
SetActorToTarget(target);
return target.Actor != null;
}
public bool IsTargetVisible =>
TargetActor != null &&
TargetActor.CanBeViewedByPlayer(Bot.Player);
public WPos CenterPosition()
{
return Units.Select(a => a.CenterPosition).Average();
}
public Actor CenterUnit()
{
var centerPosition = CenterPosition();
return Units.MinByOrDefault(a => (a.CenterPosition - centerPosition).LengthSquared);
}
public MiniYaml Serialize()
{
var nodes = new List<MiniYamlNode>()
{
new("Type", FieldSaver.FormatValue(Type)),
new("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray()))
};
if (Target.Type != TargetType.Invalid)
{
nodes.Add(new MiniYamlNode("ActorToTarget", FieldSaver.FormatValue(TargetActor.ActorID)));
nodes.Add(new MiniYamlNode("TargetOffset", FieldSaver.FormatValue(Target.CenterPosition - TargetActor.CenterPosition)));
}
return new MiniYaml("", nodes);
}
public static Squad Deserialize(IBot bot, SquadManagerBotModule squadManager, MiniYaml yaml)
{
var type = SquadType.Rush;
var target = ((Actor)null, WVec.Zero);
var typeNode = yaml.NodeWithKeyOrDefault("Type");
if (typeNode != null)
type = FieldLoader.GetValue<SquadType>("Type", typeNode.Value.Value);
var actorToTargetNode = yaml.NodeWithKeyOrDefault("ActorToTarget");
var targetOffsetNode = yaml.NodeWithKeyOrDefault("TargetOffset");
if (actorToTargetNode != null && targetOffsetNode != null)
{
var actorToTarget = squadManager.World.GetActorById(FieldLoader.GetValue<uint>("ActorToTarget", actorToTargetNode.Value.Value));
var targetOffset = FieldLoader.GetValue<WVec>("TargetOffset", targetOffsetNode.Value.Value);
target = (actorToTarget, targetOffset);
}
var squad = new Squad(bot, squadManager, type, target);
var unitsNode = yaml.NodeWithKeyOrDefault("Units");
if (unitsNode != null)
squad.Units.UnionWith(FieldLoader.GetValue<uint[]>("Units", unitsNode.Value.Value)
.Select(squadManager.World.GetActorById));
return squad;
}
}
}

View File

@@ -0,0 +1,40 @@
#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
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
sealed class StateMachine
{
IState currentState;
public void Update(Squad squad)
{
currentState?.Tick(squad);
}
public void ChangeState(Squad squad, IState newState)
{
currentState?.Deactivate(squad);
if (newState != null)
currentState = newState;
currentState?.Activate(squad);
}
}
interface IState
{
void Activate(Squad bot);
void Tick(Squad bot);
void Deactivate(Squad bot);
}
}

View File

@@ -0,0 +1,227 @@
#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.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
abstract class AirStateBase : StateBase
{
protected const int MissileUnitMultiplier = 3;
protected static int CountAntiAirUnits(Squad owner, IReadOnlyCollection<Actor> units)
{
if (units.Count == 0)
return 0;
var missileUnitsCount = 0;
foreach (var unit in units)
{
if (unit == null || unit.Info.HasTraitInfo<AircraftInfo>())
continue;
foreach (var ab in unit.TraitsImplementing<AttackBase>())
{
if (ab.IsTraitDisabled || ab.IsTraitPaused)
continue;
foreach (var a in ab.Armaments)
{
if (a.Weapon.IsValidTarget(owner.SquadManager.Info.AircraftTargetType))
{
missileUnitsCount++;
break;
}
}
}
}
return missileUnitsCount;
}
protected static Actor FindDefenselessTarget(Squad owner)
{
FindSafePlace(owner, out var target, true);
return target;
}
protected static CPos? FindSafePlace(Squad owner, out Actor detectedEnemyTarget, bool needTarget)
{
var map = owner.World.Map;
var dangerIndiceSideLength = owner.SquadManager.Info.DangerScanRadius * 141 / 100; // ¡Ö DangerScanRadius * sqrt(2)
detectedEnemyTarget = null;
var columnCount = (map.Bounds.Width + dangerIndiceSideLength - 1) / dangerIndiceSideLength;
var rowCount = (map.Bounds.Height + dangerIndiceSideLength - 1) / dangerIndiceSideLength;
var xoffset = map.Bounds.X;
var yoffset = map.Bounds.Y;
// Construct a grid of points as the center of square with side length of dangerDiameter to divide the map and shuffle them to get a random search pattern.
// Make sure when search with DangerScanRadius, covers the whole indice and covers the least cells in other indice.
foreach (var i in Exts.MakeArray(columnCount * rowCount, i => i).Shuffle(owner.World.LocalRandom))
{
var pos = new MPos(xoffset + i % columnCount * dangerIndiceSideLength + (dangerIndiceSideLength >> 1),
yoffset + i / columnCount * dangerIndiceSideLength + (dangerIndiceSideLength >> 1)).ToCPos(map);
if (NearToPosSafely(owner, map.CenterOfCell(pos), out detectedEnemyTarget))
{
if (needTarget && detectedEnemyTarget == null)
continue;
return pos;
}
}
return null;
}
protected static bool NearToPosSafely(Squad owner, WPos loc)
{
return NearToPosSafely(owner, loc, out _);
}
protected static bool NearToPosSafely(Squad owner, WPos loc, out Actor detectedEnemyTarget)
{
detectedEnemyTarget = null;
var dangerRadius = owner.SquadManager.Info.DangerScanRadius;
var unitsAroundPos = owner.World.FindActorsInCircle(loc, WDist.FromCells(dangerRadius))
.Where(owner.SquadManager.IsPreferredEnemyUnit).ToList();
if (unitsAroundPos.Count == 0)
return true;
if (CountAntiAirUnits(owner, unitsAroundPos) * MissileUnitMultiplier < owner.Units.Count)
{
detectedEnemyTarget = unitsAroundPos.Random(owner.Random);
return true;
}
return false;
}
// Checks the number of anti air enemies around units
protected virtual bool ShouldFlee(Squad owner)
{
return ShouldFlee(owner, enemies => CountAntiAirUnits(owner, enemies) * MissileUnitMultiplier > owner.Units.Count);
}
}
sealed class AirIdleState : AirStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
if (ShouldFlee(owner))
{
owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState());
return;
}
var e = FindDefenselessTarget(owner);
if (e == null)
return;
owner.SetActorToTarget((e, WVec.Zero));
owner.FuzzyStateMachine.ChangeState(owner, new AirAttackState());
}
public void Deactivate(Squad owner) { }
}
sealed class AirAttackState : AirStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
var leader = owner.CenterUnit();
if (!owner.IsTargetValid(leader))
{
var closestEnemy = owner.SquadManager.FindClosestEnemy(leader);
owner.SetActorToTarget(closestEnemy);
if (closestEnemy.Actor == null)
{
owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState());
return;
}
}
if (!NearToPosSafely(owner, owner.Units.ClosestToIgnoringPath(owner.TargetActor).CenterPosition))
{
owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState());
return;
}
foreach (var a in owner.Units)
{
if (BusyAttack(a))
continue;
var ammoPools = a.TraitsImplementing<AmmoPool>().ToArray();
if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault<Rearmable>()))
{
if (IsRearming(a))
continue;
if (!HasAmmo(ammoPools))
{
owner.Bot.QueueOrder(new Order("ReturnToBase", a, false));
continue;
}
}
if (CanAttackTarget(a, owner.TargetActor))
owner.Bot.QueueOrder(new Order("Attack", a, owner.Target, false));
}
}
public void Deactivate(Squad owner) { }
}
sealed class AirFleeState : AirStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
foreach (var a in owner.Units)
{
var ammoPools = a.TraitsImplementing<AmmoPool>().ToArray();
if (!ReloadsAutomatically(ammoPools, a.TraitOrDefault<Rearmable>()) && !FullAmmo(ammoPools))
{
if (IsRearming(a))
continue;
owner.Bot.QueueOrder(new Order("ReturnToBase", a, false));
continue;
}
owner.Bot.QueueOrder(new Order("Move", a, Target.FromCell(owner.World, RandomBuildingLocation(owner)), false));
}
owner.FuzzyStateMachine.ChangeState(owner, new AirIdleState());
}
public void Deactivate(Squad owner) { }
}
}

View File

@@ -0,0 +1,270 @@
#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.Linq;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
abstract class GroundStateBase : StateBase
{
Actor leader;
/// <summary>
/// Elects a unit to lead the squad, other units in the squad will regroup to the leader if they start to spread out.
/// The leader remains the same unless a new one is forced or the leader is no longer part of the squad.
/// </summary>
protected Actor Leader(Squad owner)
{
if (leader == null || !owner.Units.Contains(leader))
leader = NewLeader(owner);
return leader;
}
static Actor NewLeader(Squad owner)
{
IEnumerable<Actor> units = owner.Units;
// Identify the Locomotor with the most restrictive passable terrain list. For squads with mixed
// locomotors, we hope to choose the most restrictive option. This means we won't nominate a leader who has
// more options. This avoids situations where we would nominate a hovercraft as the leader and tanks would
// fail to follow it because they can't go over water. By forcing us to choose a unit with limited movement
// options, we maximise the chance other units will be able to follow it. We could still be screwed if the
// squad has a mix of units with disparate movement, e.g. land units and naval units. We must trust the
// squad has been formed from a set of units that don't suffer this problem.
var leastCommonDenominator = units
.Select(a => a.TraitOrDefault<Mobile>()?.Locomotor)
.Where(l => l != null)
.MinByOrDefault(l => l.Info.TerrainSpeeds.Count)
?.Info.TerrainSpeeds.Count;
if (leastCommonDenominator != null)
units = units.Where(a => a.TraitOrDefault<Mobile>()?.Locomotor.Info.TerrainSpeeds.Count == leastCommonDenominator).ToList();
// Choosing a unit in the center reduces the need for an immediate regroup.
var centerPosition = units.Select(a => a.CenterPosition).Average();
return units.MinBy(a => (a.CenterPosition - centerPosition).LengthSquared);
}
protected virtual bool ShouldFlee(Squad owner)
{
return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies));
}
protected (Actor Actor, WVec Offset) NewLeaderAndFindClosestEnemy(Squad owner)
{
leader = null; // Force a new leader to be elected, useful if we are targeting a new enemy.
return owner.SquadManager.FindClosestEnemy(Leader(owner));
}
protected IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable<Actor> actors)
{
return owner.SquadManager.FindEnemies(
actors,
Leader(owner));
}
protected static Actor ClosestToEnemy(Squad owner)
{
return SquadManagerBotModule.ClosestTo(owner.Units, owner.TargetActor);
}
}
sealed class GroundUnitsIdleState : GroundStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
if (!owner.IsTargetValid(Leader(owner)))
{
var closestEnemy = NewLeaderAndFindClosestEnemy(owner);
owner.SetActorToTarget(closestEnemy);
if (closestEnemy.Actor == null)
return;
}
var enemyUnits =
FindEnemies(owner,
owner.World.FindActorsInCircle(owner.Target.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)))
.Select(x => x.Actor)
.ToList();
if (enemyUnits.Count == 0)
return;
if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits))
{
owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray()));
// We have gathered sufficient units. Attack the nearest enemy unit.
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState());
}
else
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState());
}
public void Deactivate(Squad owner) { }
}
sealed class GroundUnitsAttackMoveState : GroundStateBase, IState
{
int lastUpdatedTick;
CPos? lastLeaderLocation;
Actor lastTarget;
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
if (!owner.IsTargetValid(Leader(owner)))
{
var closestEnemy = NewLeaderAndFindClosestEnemy(owner);
owner.SetActorToTarget(closestEnemy);
if (closestEnemy.Actor == null)
{
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState());
return;
}
}
var leader = Leader(owner);
if (leader.Location != lastLeaderLocation)
{
lastLeaderLocation = leader.Location;
lastUpdatedTick = owner.World.WorldTick;
}
if (owner.TargetActor != lastTarget)
{
lastTarget = owner.TargetActor;
lastUpdatedTick = owner.World.WorldTick;
}
// HACK: Drop back to the idle state if we haven't moved in 2.5 seconds
// This works around the squad being stuck trying to attack-move to a location
// that they cannot path to, generating expensive pathfinding calls each tick.
if (owner.World.WorldTick > lastUpdatedTick + 63)
{
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState());
return;
}
var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3)
.Where(owner.Units.Contains).ToHashSet();
if (ownUnits.Count < owner.Units.Count)
{
// Since units have different movement speeds, they get separated while approaching the target.
// Let them regroup into tighter formation.
owner.Bot.QueueOrder(new Order("Stop", leader, false));
var units = owner.Units.Where(a => !ownUnits.Contains(a)).ToArray();
owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: units));
}
else
{
var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius));
if (target.Actor != null)
{
owner.SetActorToTarget(target);
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState());
}
else
owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray()));
}
if (ShouldFlee(owner))
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState());
}
public void Deactivate(Squad owner) { }
}
sealed class GroundUnitsAttackState : GroundStateBase, IState
{
int lastUpdatedTick;
CPos? lastLeaderLocation;
Actor lastTarget;
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
if (!owner.IsTargetValid(Leader(owner)))
{
var closestEnemy = NewLeaderAndFindClosestEnemy(owner);
owner.SetActorToTarget(closestEnemy);
if (closestEnemy.Actor == null)
{
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState());
return;
}
}
var leader = Leader(owner);
if (leader.Location != lastLeaderLocation)
{
lastLeaderLocation = leader.Location;
lastUpdatedTick = owner.World.WorldTick;
}
if (owner.TargetActor != lastTarget)
{
lastTarget = owner.TargetActor;
lastUpdatedTick = owner.World.WorldTick;
}
// HACK: Drop back to the idle state if we haven't moved in 2.5 seconds
// This works around the squad being stuck trying to attack-move to a location
// that they cannot path to, generating expensive pathfinding calls each tick.
if (owner.World.WorldTick > lastUpdatedTick + 63)
{
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState());
return;
}
foreach (var a in owner.Units)
if (!BusyAttack(a))
owner.Bot.QueueOrder(new Order("AttackMove", a, owner.Target, false));
if (ShouldFlee(owner))
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState());
}
public void Deactivate(Squad owner) { }
}
sealed class GroundUnitsFleeState : GroundStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
GoToRandomOwnBuilding(owner);
owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState());
}
public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); }
}
}

View File

@@ -0,0 +1,84 @@
#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.Linq;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
sealed class UnitsForProtectionIdleState : GroundStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner) { owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionAttackState()); }
public void Deactivate(Squad owner) { }
}
sealed class UnitsForProtectionAttackState : GroundStateBase, IState
{
public const int BackoffTicks = 4;
internal int Backoff = BackoffTicks;
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
var leader = Leader(owner);
if (!owner.IsTargetValid(leader))
{
var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius));
owner.SetActorToTarget(target);
if (target.Actor == null)
{
owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState());
return;
}
}
owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray()));
if (!owner.IsTargetVisible)
{
if (Backoff < 0)
{
owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState());
Backoff = BackoffTicks;
return;
}
Backoff--;
}
else
{
Backoff = BackoffTicks;
}
}
public void Deactivate(Squad owner) { }
}
sealed class UnitsForProtectionFleeState : GroundStateBase, IState
{
public void Activate(Squad owner) { }
public void Tick(Squad owner)
{
if (!owner.IsValid)
return;
GoToRandomOwnBuilding(owner);
owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionIdleState());
}
public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); }
}
}

View File

@@ -0,0 +1,141 @@
#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.Mods.Common.Activities;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.BotModules.Squads
{
abstract class StateBase
{
protected static void GoToRandomOwnBuilding(Squad squad)
{
var loc = RandomBuildingLocation(squad);
foreach (var a in squad.Units)
squad.Bot.QueueOrder(new Order("Move", a, Target.FromCell(squad.World, loc), false));
}
protected static CPos RandomBuildingLocation(Squad squad)
{
var location = squad.SquadManager.GetRandomBaseCenter();
var buildings = squad.World.ActorsHavingTrait<Building>()
.Where(a => a.Owner == squad.Bot.Player).ToList();
if (buildings.Count > 0)
location = buildings.Random(squad.Random).Location;
return location;
}
protected static bool BusyAttack(Actor a)
{
if (a.IsIdle)
return false;
var activity = a.CurrentActivity;
var type = activity.GetType();
if (type == typeof(Attack) || type == typeof(FlyAttack))
return true;
var next = activity.NextActivity;
if (next == null)
return false;
var nextType = next.GetType();
if (nextType == typeof(Attack) || nextType == typeof(FlyAttack))
return true;
return false;
}
protected static bool CanAttackTarget(Actor a, Actor target)
{
if (!a.Info.HasTraitInfo<AttackBaseInfo>())
return false;
var targetTypes = target.GetEnabledTargetTypes();
if (targetTypes.IsEmpty)
return false;
var arms = a.TraitsImplementing<Armament>();
foreach (var arm in arms)
{
if (arm.IsTraitDisabled)
continue;
if (arm.Weapon.IsValidTarget(targetTypes))
return true;
}
return false;
}
protected virtual bool ShouldFlee(Squad squad, Func<IReadOnlyCollection<Actor>, bool> flee)
{
if (!squad.IsValid)
return false;
var dangerRadius = squad.SquadManager.Info.DangerScanRadius;
var units = squad.World.FindActorsInCircle(squad.CenterPosition(), WDist.FromCells(dangerRadius)).ToList();
// If there are any own buildings within the DangerRadius, don't flee
// PERF: Avoid LINQ
foreach (var u in units)
if (u.Owner == squad.Bot.Player && u.Info.HasTraitInfo<BuildingInfo>())
return false;
var enemyAroundUnit = units
.Where(unit => squad.SquadManager.IsPreferredEnemyUnit(unit) && unit.Info.HasTraitInfo<AttackBaseInfo>())
.ToList();
if (enemyAroundUnit.Count == 0)
return false;
return flee(enemyAroundUnit);
}
protected static bool IsRearming(Actor a)
{
return !a.IsIdle && (a.CurrentActivity.ActivitiesImplementing<Resupply>().Any() || a.CurrentActivity.ActivitiesImplementing<ReturnToBase>().Any());
}
protected static bool FullAmmo(IEnumerable<AmmoPool> ammoPools)
{
foreach (var ap in ammoPools)
if (!ap.HasFullAmmo)
return false;
return true;
}
protected static bool HasAmmo(IEnumerable<AmmoPool> ammoPools)
{
foreach (var ap in ammoPools)
if (!ap.HasAmmo)
return false;
return true;
}
protected static bool ReloadsAutomatically(IEnumerable<AmmoPool> ammoPools, Rearmable rearmable)
{
if (rearmable == null)
return true;
foreach (var ap in ammoPools)
if (!rearmable.Info.AmmoPools.Contains(ap.Info.Name))
return false;
return true;
}
}
}