From 14527d11a683b8aa5ea180e5e716a948b813d3c9 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sat, 27 Jun 2026 19:44:57 +0800 Subject: [PATCH 1/6] Add ExternalBotOrdersManager and IssueOrderToBot --- .../BotModules/ExternalBotOrdersManager.cs | 160 ++++++++++++++++++ .../Traits/BotModules/IssueOrderToBot.cs | 139 +++++++++++++++ mods/ca/rules/ai.yaml | 2 + 3 files changed, 301 insertions(+) create mode 100644 OpenRA.Mods.CA/Traits/BotModules/ExternalBotOrdersManager.cs create mode 100644 OpenRA.Mods.CA/Traits/BotModules/IssueOrderToBot.cs diff --git a/OpenRA.Mods.CA/Traits/BotModules/ExternalBotOrdersManager.cs b/OpenRA.Mods.CA/Traits/BotModules/ExternalBotOrdersManager.cs new file mode 100644 index 0000000000..fcbbd69ba9 --- /dev/null +++ b/OpenRA.Mods.CA/Traits/BotModules/ExternalBotOrdersManager.cs @@ -0,0 +1,160 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.CA.Traits +{ + [Flags] + public enum TargetDistance + { + Closest = 0, + Furthest = 1, + Random = 2 + } + + [Desc("Allows the player to issue the orders from actors with " + nameof(IssueOrderToBot) + ". etc")] + public class ExternalBotOrdersManagerInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new ExternalBotOrdersManager(init.Self, this); } + } + + public class ExternalBotOrdersManager : ConditionalTrait, IBotTick + { + readonly Dictionary directEntries = new(); + readonly Dictionary issueOrderToBotEntries = new(); + readonly World world; + readonly Player player; + + public bool ManagerRunning { get; private set; } + + public ExternalBotOrdersManager(Actor self, ExternalBotOrdersManagerInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + ManagerRunning = false; + } + + // Use for proactive targeting. + public bool IsPreferredTarget(Actor self, Actor a, + PlayerRelationship validRelationships, BitSet validTargets, BitSet invalidTargets) + { + if (a == self || a == null || a.IsDead || !a.IsInWorld || !validRelationships.HasRelationship(self.Owner.RelationshipWith(a.Owner))) + return false; + + var targetTypes = a.GetEnabledTargetTypes(); + return !targetTypes.IsEmpty && validTargets.Overlaps(targetTypes) && !invalidTargets.Overlaps(targetTypes); + } + + public bool IsNotHiddenUnit(Actor self, Actor a) + { + var hasModifier = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, self.Owner)) + return true; + + hasModifier = true; + } + + return !hasModifier; + } + + public void AddEntry(Actor issuer, Order order, int chance) + { + directEntries[issuer] = (order, chance); + } + + public void AddEntryFromIssueOrderToBot(Actor issuer, IssueOrderToBotInfo info) + { + issueOrderToBotEntries[issuer] = info; + } + + public Order PhraseEntryFromIssueOrderToBot(Actor issuer, IssueOrderToBotInfo info) + { + if (issuer.IsDead || !issuer.IsInWorld || issuer.Owner != player || world.LocalRandom.Next(100) > info.OrderChance) + return null; + + var subject = info.IsIssuerOwner ? issuer.Owner.PlayerActor : issuer; + Order order = null; + + if (info.TargetRangeInCell > 0) + { + var validActors = world.FindActorsInCircle(issuer.CenterPosition, WDist.FromCells(info.TargetRangeInCell)) + .Where(a => IsPreferredTarget(issuer, a, info.ValidRelationships, info.ValidTargets, info.InvalidTargets) && IsNotHiddenUnit(issuer, a)); + + Actor validActor = null; + + switch (info.TargetDistance) + { + case TargetDistance.Closest: + validActor = validActors.ClosestToIgnoringPath(issuer); + break; + case TargetDistance.Furthest: + validActor = validActors.OrderByDescending(a => (a.Location - issuer.Location).LengthSquared).FirstOrDefault(); + break; + case TargetDistance.Random: + validActor = validActors.RandomOrDefault(world.LocalRandom); + break; + } + + if (validActor == null) + return null; + + var target = Target.Invalid; + if (info.UseTargetLocation) + order = new Order(info.OrderName, subject, Target.FromCell(world, validActor.Location), false); + else + order = new Order(info.OrderName, subject, Target.FromActor(validActor), false); + } + + return order ??= new Order(info.OrderName, subject, false); + } + + void IBotTick.BotTick(IBot bot) + { + // "ManagerRunning = true" means IBotTick is running, and the game is + // 1. not a replay + // 2. not saved game still loading + // 3. the game running on the host where AI is enabled + ManagerRunning = true; + + foreach (var entry in directEntries) + { + if (entry.Key.IsDead || !entry.Key.IsInWorld || entry.Key.Owner != player) + continue; + + if (world.LocalRandom.Next(100) > entry.Value.Chance) + continue; + + bot.QueueOrder(entry.Value.Order); + } + + directEntries.Clear(); + + foreach (var entry in issueOrderToBotEntries) + { + var order = PhraseEntryFromIssueOrderToBot(entry.Key, entry.Value); + if (order != null) + bot.QueueOrder(order); + } + + issueOrderToBotEntries.Clear(); + } + } +} diff --git a/OpenRA.Mods.CA/Traits/BotModules/IssueOrderToBot.cs b/OpenRA.Mods.CA/Traits/BotModules/IssueOrderToBot.cs new file mode 100644 index 0000000000..b8104e18aa --- /dev/null +++ b/OpenRA.Mods.CA/Traits/BotModules/IssueOrderToBot.cs @@ -0,0 +1,139 @@ +#region Copyright & License Information +/** + * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). + * This file is part of OpenRA Combined Arms, 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 OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.CA.Traits +{ + [Flags] + public enum OrderTriggers + { + None = 0, + Attack = 1, + Damage = 2, + Heal = 4, + Periodically = 8, + BecomingIdle = 16 + } + + [Desc("Allow this actor to automatically issue orders to bot player and processed by " + nameof(ExternalBotOrdersManager))] + public class IssueOrderToBotInfo : ConditionalTraitInfo + { + [Desc("Events leading to the actor issue order. Possible values are: None, Attack, Damage, Heal, Periodically, BecomingIdle.")] + public readonly OrderTriggers OrderTrigger = OrderTriggers.Attack | OrderTriggers.Damage; + + [FieldLoader.Require] + [Desc("Order name to issue.")] + public readonly string OrderName = null; + + [Desc("Chance of the order take effect.")] + public readonly int OrderChance = 50; + + [Desc("Order's target range. <0 means disable targeting.")] + public readonly int TargetRangeInCell = -1; + + [Desc("Should the player be the subject of the order. It is the actor by default.", "Can use this to issue support power order.")] + public readonly bool IsIssuerOwner = false; + + [Desc("What player relationships are affected.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Enemy; + + [Desc("What types of targets are affected.")] + public readonly BitSet ValidTargets; + + [Desc("What types of targets are unaffected.", "Overrules ValidTargets.")] + public readonly BitSet InvalidTargets; + + [Desc("Use Target's Location.")] + public readonly bool UseTargetLocation = true; + + [Desc("Delay between two successful issued orders.")] + public readonly int OrderInterval = 2500; + + [Desc("Should attack the furthest or closest target. Possible values are Closest, Furthest, Random", + "Multiple values mean the distance randomizes between them")] + public readonly TargetDistance TargetDistance = TargetDistance.Closest; + + public override object Create(ActorInitializer init) { return new IssueOrderToBot(this); } + } + + public class IssueOrderToBot : ConditionalTrait, INotifyAttack, ITick, INotifyDamage, + INotifyCreated, INotifyOwnerChanged, INotifyBecomingIdle + { + int orderTicks; + ExternalBotOrdersManager orderManager; + + public IssueOrderToBot(IssueOrderToBotInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + orderManager = self.Owner.PlayerActor.Trait(); + } + + void TryIssueOrder(Actor self) + { + if (!orderManager.ManagerRunning || orderTicks > 0 || orderManager.IsTraitDisabled) + return; + + orderManager.AddEntryFromIssueOrderToBot(self, Info); + orderTicks = Info.OrderInterval; + } + + void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (Info.OrderTrigger.HasFlag(OrderTriggers.Attack)) + TryIssueOrder(self); + } + + void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } + + void ITick.Tick(Actor self) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (--orderTicks < 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Periodically)) + TryIssueOrder(self); + } + + void INotifyDamage.Damaged(Actor self, AttackInfo e) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (e.Damage.Value > 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Damage)) + TryIssueOrder(self); + + if (e.Damage.Value < 0 && Info.OrderTrigger.HasFlag(OrderTriggers.Heal)) + TryIssueOrder(self); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + orderManager = newOwner.PlayerActor.Trait(); + } + + void INotifyBecomingIdle.OnBecomingIdle(Actor self) + { + if (!orderManager.ManagerRunning || IsTraitDisabled || orderManager.IsTraitDisabled) + return; + + if (Info.OrderTrigger.HasFlag(OrderTriggers.BecomingIdle)) + TryIssueOrder(self); + } + } +} diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index 68c186dd3b..989bf3754f 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -35,6 +35,8 @@ Player: GrantConditionOnBotOwner@NavalAI: Condition: enable-naval-ai Bots: naval + ExternalBotOrdersManager: + RequiresCondition: enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai SupportPowerBotModule: RequiresCondition: enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai Decisions: From ffaa4f3c727478b1b04e44142d3daa4fa94766cd Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sat, 27 Jun 2026 20:43:53 +0800 Subject: [PATCH 2/6] Allow bot to use TargetedAttackAbility, SpawnActorAbility --- mods/ca/rules/ai.yaml | 5 +++++ mods/ca/rules/infantry.yaml | 33 +++++++++++++++++++++++++++++++++ mods/ca/rules/scrin.yaml | 10 ++++++++++ mods/ca/rules/vehicles.yaml | 10 ++++++++++ 4 files changed, 58 insertions(+) diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index 989bf3754f..df5f201018 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -1074,6 +1074,7 @@ Player: hazmat.upgrade: 1 hazmatsoviet.upgrade: 1 howi.upgrade: 1 + decoy.upgrade: 1 microwave.upgrade: 1 blacknapalm.upgrade: 1 quantum.upgrade: 1 @@ -1345,6 +1346,7 @@ Player: hazmat.upgrade: 1 hazmatsoviet.upgrade: 1 howi.upgrade: 1 + decoy.upgrade: 1 microwave.upgrade: 1 blacknapalm.upgrade: 1 quantum.upgrade: 1 @@ -1615,6 +1617,7 @@ Player: hazmat.upgrade: 1 hazmatsoviet.upgrade: 1 howi.upgrade: 1 + decoy.upgrade: 1 microwave.upgrade: 1 blacknapalm.upgrade: 1 quantum.upgrade: 1 @@ -1885,6 +1888,7 @@ Player: hazmat.upgrade: 1 hazmatsoviet.upgrade: 1 howi.upgrade: 1 + decoy.upgrade: 1 microwave.upgrade: 1 blacknapalm.upgrade: 1 quantum.upgrade: 1 @@ -2119,6 +2123,7 @@ Player: hazmat.upgrade: 1 hazmatsoviet.upgrade: 1 howi.upgrade: 1 + decoy.upgrade: 1 microwave.upgrade: 1 blacknapalm.upgrade: 1 quantum.upgrade: 1 diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 671e983861..64e057d99e 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -2631,6 +2631,16 @@ CONF: ConcurrentLimit: 1 KillExcessDamageTypes: SmallExplosionDeath AvoidActors: true + IssueOrderToBot: + RequiresCondition: !being-warped && !blinded && !reapersnare && !parachute && !berserk + OrderName: SpawnActorAbility + OrderTrigger: BecomingIdle, Attack + OrderChance: 100 + OrderInterval: 760 + TargetRangeInCell: 3 + ValidTargets: Infantry, Cyborg + ValidRelationships: Ally + TargetDistance: Closest AmmoPool@IdolOfKane: Name: idol Armaments: none @@ -4469,6 +4479,18 @@ ENLI: AmmoPool: Armaments: secondary Ammo: 1 + IssueOrderToBot: + RequiresCondition: !being-warped && !empdisable && !blinded && !parachute && !reapersnare && !berserk + OrderName: TargetedAttackAbilityAttack + OrderTrigger: Attack + UseTargetLocation: false + OrderChance: 100 + OrderInterval: 505 + TargetRangeInCell: 7 + ValidTargets: Vehicle, Ship, Building + InvalidTargets: Cyborg, EmpImmune + ValidRelationships: Enemy + TargetDistance: Random ReloadAmmoPoolCA: Delay: 500 # equal to reload time of weapon Count: 1 @@ -5377,6 +5399,17 @@ REAP: Count: 1 ShowSelectionBar: true SelectionBarColor: 00cc00 + IssueOrderToBot: + RequiresCondition: !being-warped && !empdisable && !blinded && !parachute && !reapersnare && !berserk + OrderName: TargetedAttackAbilityAttack + OrderTrigger: Attack + UseTargetLocation: false + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 8 + ValidTargets: Infantry, Cyborg + ValidRelationships: Enemy + TargetDistance: Random Encyclopedia: Category: Nod/Infantry EncyclopediaExtras: diff --git a/mods/ca/rules/scrin.yaml b/mods/ca/rules/scrin.yaml index 892f46f9c5..183629f867 100644 --- a/mods/ca/rules/scrin.yaml +++ b/mods/ca/rules/scrin.yaml @@ -1483,6 +1483,16 @@ MAST: Behavior: SpawnAtSelfAndSlavesAndMoveToTarget RequiresCondition: !being-warped && mspk-ammo AmmoPool: mspk + IssueOrderToBot: + RequiresCondition: !being-warped && mspk-ammo && !parachute && !reapersnare + OrderName: SpawnActorAbility + OrderTrigger: Attack + OrderChance: 100 + OrderInterval: 80 ## When this trait is disabled, the countdown will stop + TargetRangeInCell: 12 + ValidTargets: Infantry, Vehicle + ValidRelationships: Enemy + TargetDistance: Random AmmoPool@MindSpark: Name: mspk Ammo: 1 diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index 55d622ff11..044acd4e6e 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -3552,6 +3552,16 @@ BGGY: RequiresCondition: player-blackh && decoy-upgrade && ammo && !(empdisable || being-warped) AmmoPool: decoyspawner FaceTarget: true + IssueOrderToBot: + RequiresCondition: decoy-upgrade && ammo && !(empdisable || being-warped) + OrderName: SpawnActorAbility + OrderTrigger: Attack + OrderChance: 100 + OrderInterval: 760 + TargetRangeInCell: 12 + ValidTargets: Infantry, Vehicle, Defense + ValidRelationships: Enemy + TargetDistance: Random ReplacedInQueue: Actors: rbug Upgradeable@Raider: From 00336d1b24019eb1b154abc7486604fad45d58e2 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sat, 27 Jun 2026 21:34:21 +0800 Subject: [PATCH 3/6] Allow bot to use GrantTimedConditionOnDeploy --- mods/ca/rules/ai.yaml | 10 ++++++++++ mods/ca/rules/infantry.yaml | 12 ++++++++++++ mods/ca/rules/scrin.yaml | 12 ++++++++++++ mods/ca/rules/structures.yaml | 9 +++++++++ mods/ca/rules/vehicles.yaml | 12 ++++++++++++ 5 files changed, 55 insertions(+) diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index df5f201018..9700cad729 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -1099,6 +1099,7 @@ Player: gattling.upgrade: 1 charv.upgrade: 1 pcan.upgrade: 1 + optics.upgrade: 1 airborne.upgrade: 1 cryw.upgrade: 1 apb.upgrade: 1 @@ -1111,6 +1112,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 rebel.allegiance: 1 @@ -1371,6 +1373,7 @@ Player: gattling.upgrade: 1 charv.upgrade: 1 pcan.upgrade: 1 + optics.upgrade: 1 airborne.upgrade: 1 cryw.upgrade: 1 apb.upgrade: 1 @@ -1383,6 +1386,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 rebel.allegiance: 1 @@ -1642,6 +1646,7 @@ Player: gattling.upgrade: 1 charv.upgrade: 1 pcan.upgrade: 1 + optics.upgrade: 1 airborne.upgrade: 1 cryw.upgrade: 1 apb.upgrade: 1 @@ -1654,6 +1659,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 rebel.allegiance: 1 @@ -1913,6 +1919,7 @@ Player: gattling.upgrade: 1 charv.upgrade: 1 pcan.upgrade: 1 + optics.upgrade: 1 airborne.upgrade: 1 cryw.upgrade: 1 apb.upgrade: 1 @@ -1925,6 +1932,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 rebel.allegiance: 1 @@ -2148,6 +2156,7 @@ Player: gattling.upgrade: 1 charv.upgrade: 1 pcan.upgrade: 1 + optics.upgrade: 1 airborne.upgrade: 1 cryw.upgrade: 1 apb.upgrade: 1 @@ -2160,6 +2169,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 rebel.allegiance: 1 diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 64e057d99e..525389a58a 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -1992,6 +1992,12 @@ N5: ChargingColor: 008800 Instant: true DeploySound: tibsurge.aud + IssueOrderToBot: + RequiresCondition: !being-warped && !reapersnare && !parachute && !berserk + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Attack, Damage + OrderChance: 100 + OrderInterval: 830 Targetable@ChemWarrior: TargetTypes: ChemWarrior SpeedMultiplier@BlackHand: @@ -4953,6 +4959,12 @@ ZDEF: UndeploySound: gshielddown.aud Voice: Shield Instant: true + IssueOrderToBot: + RequiresCondition: !being-warped && !reapersnare && !parachute && !berserk + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Damage + OrderChance: 100 + OrderInterval: 955 WithRadiatingCircle@ZoneDefenderShield: EndRadius: 2c0 Color: 00eecc10 diff --git a/mods/ca/rules/scrin.yaml b/mods/ca/rules/scrin.yaml index 183629f867..44d4c89c63 100644 --- a/mods/ca/rules/scrin.yaml +++ b/mods/ca/rules/scrin.yaml @@ -286,6 +286,12 @@ VEHICLES.SCRIN: DischargeOnAttack: true MaxTicksBeforeDischarge: 375 Instant: true + IssueOrderToBot: + RequiresCondition: hyper-upgrade && !being-warped && !empdisable && !berserk + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Attack + OrderChance: 50 + OrderInterval: 605 WithPalettedOverlay@HYPERCHARGE: RequiresCondition: hypercharged && !(empdisable || invulnerability || invisibility) Palette: hypercharge @@ -1067,6 +1073,12 @@ STLK: ChargingColor: 808080 DischargingColor: ffffff Instant: true + IssueOrderToBot: + OrderName: GrantTimedConditionOnDeploy + RequiresCondition: !being-warped && !reapersnare && !parachute && !berserk + OrderTrigger: Periodically + OrderChance: 100 + OrderInterval: 880 Cloak@NORMAL: InitialDelay: 0 CloakDelay: 750 diff --git a/mods/ca/rules/structures.yaml b/mods/ca/rules/structures.yaml index 5c159a1097..3146941307 100644 --- a/mods/ca/rules/structures.yaml +++ b/mods/ca/rules/structures.yaml @@ -6440,6 +6440,15 @@ IOK: Instant: true StartsFullyCharged: true RequiresCondition: initialized + IssueOrderToBot: ## works like AutoTarget for bots + RequiresCondition: initialized && !build-incomplete + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Periodically + TargetRangeInCell: 5 + ValidTargets: Cyborg, Vehicle, Ship, Infantry + InvalidTargets: ChaosImmune + OrderChance: 100 + OrderInterval: 60 WithColoredOverlay@Rage: RequiresCondition: rage-active Color: FF000033 diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index 044acd4e6e..801ebf62df 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -2281,6 +2281,12 @@ JEEP: DeploySound: optics-enable.aud UndeploySound: optics-disable.aud Instant: true + IssueOrderToBot: + RequiresCondition: optics-upgrade && !empdisable && !being-warped && !berserk + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Periodically + OrderChance: 100 + OrderInterval: 1505 GrantConditionOnPrerequisite@Optics: Condition: optics-upgrade Prerequisites: optics.upgrade @@ -4850,6 +4856,12 @@ HTNK.Hover: DischargingColor: ff6600 DeploySound: htnkabur.aud Instant: true + IssueOrderToBot: + RequiresCondition: abur-upgrade && !being-warped && !empdisable && !berserk + OrderName: GrantTimedConditionOnDeploy + OrderTrigger: Attack, Damage + OrderChance: 100 + OrderInterval: 1705 GrantConditionOnPrerequisite@Afterburner: Condition: abur-upgrade Prerequisites: abur.upgrade From 52a08ea6875ce2a62ede7ce69499967e90d96140 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sat, 27 Jun 2026 21:16:15 +0800 Subject: [PATCH 4/6] Allow bot to use PortableChronoCA --- mods/ca/rules/ai.yaml | 5 +++++ mods/ca/rules/scrin.yaml | 30 ++++++++++++++++++++++++++++++ mods/ca/rules/vehicles.yaml | 20 ++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index 9700cad729..bc63b90ba2 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -1112,6 +1112,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + blink.upgrade: 1 hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 @@ -1386,6 +1387,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + blink.upgrade: 1 hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 @@ -1659,6 +1661,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + blink.upgrade: 1 hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 @@ -1932,6 +1935,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + blink.upgrade: 1 hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 @@ -2169,6 +2173,7 @@ Player: flakarmor.upgrade: 1 carapace.upgrade: 1 advart.upgrade: 1 + blink.upgrade: 1 hyper.upgrade: 1 shrw.upgrade: 1 loyalist.allegiance: 1 diff --git a/mods/ca/rules/scrin.yaml b/mods/ca/rules/scrin.yaml index 44d4c89c63..6637d1130b 100644 --- a/mods/ca/rules/scrin.yaml +++ b/mods/ca/rules/scrin.yaml @@ -669,6 +669,16 @@ S4: ConditionDuration: 15 ShowSelectionBarWhenFull: false CircleColor: c8b8ed66 + IssueOrderToBot: + OrderName: PortableChronoTeleport + OrderTrigger: Damage + RequiresCondition: blink-upgrade && !being-warped && !reapersnare && !parachute && !berserk + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 15 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest GrantConditionOnPrerequisite@BLINKPACKS: Condition: blink-upgrade Prerequisites: blink.upgrade @@ -733,6 +743,16 @@ MRDR: ConditionDuration: 15 ShowSelectionBarWhenFull: false CircleColor: c8b8ed66 + IssueOrderToBot: + OrderName: PortableChronoTeleport + OrderTrigger: Damage + RequiresCondition: blink-upgrade && !being-warped && !reapersnare && !parachute && !berserk + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 15 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest GrantConditionOnPrerequisite@BLINKPACKS: Condition: blink-upgrade Prerequisites: blink.upgrade @@ -829,6 +849,16 @@ WCHR: ConditionDuration: 3 ShowSelectionBarWhenFull: false CircleColor: c8b8ed66 + IssueOrderToBot: + OrderName: PortableChronoTeleport + OrderTrigger: Damage + RequiresCondition: blink-upgrade && !being-warped && !reapersnare && !parachute && !berserk + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 15 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest GrantConditionOnPrerequisite@BLINKPACKS: Condition: blink-upgrade Prerequisites: blink.upgrade diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index 801ebf62df..a598229a16 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -3068,6 +3068,16 @@ CTNK: MaxInstantCircleBorderColor: 00000044 TeleportCondition: temporal-flux-init ConditionDuration: 1 + IssueOrderToBot: + OrderName: PortableChronoTeleport + OrderTrigger: Damage + RequiresCondition: !empdisable && !being-warped && !chronoshifted && !berserk + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 12 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest WithIdleOverlay@PreCharge: Sequence: idle Image: chronoprep-overlay @@ -9222,6 +9232,16 @@ CHPR: ShowSelectionBarWhenFull: false TeleportCondition: temporal-flux-init ConditionDuration: 1 + IssueOrderToBot: + OrderName: PortableChronoTeleport + OrderTrigger: Damage + RequiresCondition: hp-below-50-perc && !empdisable && !being-warped && !chronoshifted && !berserk + OrderChance: 100 + OrderInterval: 380 + TargetRangeInCell: 12 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest GrantTimedCondition@TemporalFlux: RequiresCondition: (temporal-flux-init || chronoshifted) && tflx-upgrade RejectsOrders@AICHPR: From 3a5091632afbc13df972fcac26da96bb324b0ec9 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sat, 27 Jun 2026 21:52:22 +0800 Subject: [PATCH 5/6] Allow bot to use TargetedLeapOrderLeap --- mods/ca/rules/infantry.yaml | 13 +++++++++++++ mods/ca/rules/vehicles.yaml | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 525389a58a..25603d5706 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -4862,6 +4862,16 @@ ZTRP: MaxDistance: 7 Speed: 175 RequiresCondition: !being-warped && !reapersnare + IssueOrderToBot: + OrderName: TargetedLeapOrderLeap + OrderTrigger: Damage + RequiresCondition: !being-warped && !reapersnare && !parachute && !berserk + OrderChance: 100 + OrderInterval: 255 + TargetRangeInCell: 7 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest Contrail@Jumping: Offset: -30,0,350 StartColorUsePlayerColor: false @@ -4913,6 +4923,8 @@ ZRAI: TargetedLeapAbility: MaxDistance: 10 Speed: 200 + IssueOrderToBot: + TargetRangeInCell: 10 EncyclopediaExtras: AdditionalInfo: Requires Seek & Destroy Strategy. @@ -4937,6 +4949,7 @@ ZDEF: Health: HP: 20000 -TargetedLeapAbility: + -IssueOrderToBot: -Contrail@Jumping: -WithShadow@Jump: -Targetable@Jump: diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index a598229a16..86004fc0bb 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -9868,6 +9868,16 @@ XO: CircleColor: ffaa0077 MaxDistance: 7 Speed: 140 + IssueOrderToBot: + OrderName: TargetedLeapOrderLeap + OrderTrigger: Damage + RequiresCondition: !being-warped && !empdisable && !parachute && !berserk + OrderChance: 100 + OrderInterval: 255 + TargetRangeInCell: 7 + ValidTargets: Ground, Air + ValidRelationships: Ally + TargetDistance: Furthest Contrail@Jumping: Offset: -30,0,350 StartColorUsePlayerColor: false From ac262028d301678de438554dedbf8204aa9b2fce Mon Sep 17 00:00:00 2001 From: dnqbob Date: Sun, 28 Jun 2026 17:10:42 +0800 Subject: [PATCH 6/6] Replace and remove old AutoDeployer series --- OpenRA.Mods.CA/Traits/AutoDeployer.cs | 156 ------------------ .../Traits/Player/AutoDeployManager.cs | 79 --------- mods/ca/rules/infantry.yaml | 35 ++-- mods/ca/rules/player.yaml | 1 - mods/ca/rules/vehicles.yaml | 6 + 5 files changed, 24 insertions(+), 253 deletions(-) delete mode 100644 OpenRA.Mods.CA/Traits/AutoDeployer.cs delete mode 100644 OpenRA.Mods.CA/Traits/Player/AutoDeployManager.cs diff --git a/OpenRA.Mods.CA/Traits/AutoDeployer.cs b/OpenRA.Mods.CA/Traits/AutoDeployer.cs deleted file mode 100644 index 21cab307b6..0000000000 --- a/OpenRA.Mods.CA/Traits/AutoDeployer.cs +++ /dev/null @@ -1,156 +0,0 @@ -#region Copyright & License Information -/** - * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). - * This file is part of OpenRA Combined Arms, 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.Linq; -using OpenRA.Mods.Common.Traits; -using OpenRA.Traits; - -namespace OpenRA.Mods.CA.Traits -{ - [Flags] - public enum DeployTriggers - { - None = 0, - Attack = 1, - Damage = 2, - Heal = 4, - Periodically = 8, - BecomingIdle = 16 - } - - [Desc("Allow this actor to automatically issue deploy orders on selected events. Require the AutoDeployManager trait on the player actor.")] - public class AutoDeployerInfo : ConditionalTraitInfo - { - [Desc("Events leading to the actor getting uncloaked. Possible values are: None, Attack, Damage, Heal, Periodically, BecomingIdle.")] - public readonly DeployTriggers DeployTrigger = DeployTriggers.Attack | DeployTriggers.Damage; - - [Desc("Chance of deploying when the trigger activates.")] - public readonly int DeployChance = 50; - - [Desc("Delay between two successful deploy orders.")] - public readonly int DeployTicks = 2500; - - [Desc("Delay to wait for the actor to undeploy (if capable to) after a successful deploy.")] - public readonly int UndeployTicks = 450; - - public override object Create(ActorInitializer init) { return new AutoDeployer(this); } - } - - // TO-DO: Pester OpenRA to allow INotifyDeployTrigger to be used for other traits besides WithMakeAnimation. Like this one. - public class AutoDeployer : ConditionalTrait, INotifyAttack, ITick, INotifyDamage, INotifyCreated, ISync, INotifyOwnerChanged, INotifyDeployComplete, INotifyBecomingIdle - { - public const string PrimaryBuildingOrderID = "PrimaryProducer"; - - [Sync] - int undeployTicks = -1, deployTicks; - - bool deployed; - public bool PrimaryBuilding; - public IIssueDeployOrder[] DeployTraits; - AutoDeployManager autoDeployManager; - - public AutoDeployer(AutoDeployerInfo info) - : base(info) { } - - protected override void Created(Actor self) - { - DeployTraits = self.TraitsImplementing().ToArray(); - PrimaryBuilding = self.Info.HasTraitInfo(); - autoDeployManager = self.Owner.PlayerActor.Trait(); - } - - void TryDeploy(Actor self) - { - if (deployTicks > 0 || autoDeployManager.IsTraitDisabled) - return; - - autoDeployManager.AddEntry(new TraitPair(self, this)); - - deployTicks = Info.DeployTicks; - undeployTicks = Info.UndeployTicks; - } - - void Undeploy(Actor self) - { - if (autoDeployManager.IsTraitDisabled) - return; - - autoDeployManager.AddUndeployOrders(new Order("GrantConditionOnDeploy", self, false)); - } - - void INotifyAttack.Attacking(Actor self, in Target target, Armament a, Barrel barrel) - { - if (IsTraitDisabled || autoDeployManager.IsTraitDisabled) - return; - - if (Info.DeployTrigger.HasFlag(DeployTriggers.Attack)) - TryDeploy(self); - } - - void INotifyAttack.PreparingAttack(Actor self, in Target target, Armament a, Barrel barrel) { } - - void ITick.Tick(Actor self) - { - if (IsTraitDisabled || autoDeployManager.IsTraitDisabled) - return; - - if (deployed) - { - if (--undeployTicks < 0) - { - Undeploy(self); - deployed = false; - } - - return; - } - - if (Info.DeployTrigger.HasFlag(DeployTriggers.Periodically) && --deployTicks < 0) - TryDeploy(self); - } - - void INotifyDamage.Damaged(Actor self, AttackInfo e) - { - if (IsTraitDisabled || autoDeployManager.IsTraitDisabled) - return; - - if (e.Damage.Value > 0 && Info.DeployTrigger.HasFlag(DeployTriggers.Damage)) - TryDeploy(self); - - if (e.Damage.Value < 0 && Info.DeployTrigger.HasFlag(DeployTriggers.Heal)) - TryDeploy(self); - } - - void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) - { - autoDeployManager = newOwner.PlayerActor.Trait(); - } - - void INotifyDeployComplete.FinishedDeploy(Actor self) - { - deployed = true; - } - - void INotifyDeployComplete.FinishedUndeploy(Actor self) - { - deployed = false; - } - - void INotifyBecomingIdle.OnBecomingIdle(Actor self) - { - if (IsTraitDisabled || autoDeployManager.IsTraitDisabled) - return; - - if (Info.DeployTrigger.HasFlag(DeployTriggers.BecomingIdle)) - TryDeploy(self); - } - } -} diff --git a/OpenRA.Mods.CA/Traits/Player/AutoDeployManager.cs b/OpenRA.Mods.CA/Traits/Player/AutoDeployManager.cs deleted file mode 100644 index d76c745e9a..0000000000 --- a/OpenRA.Mods.CA/Traits/Player/AutoDeployManager.cs +++ /dev/null @@ -1,79 +0,0 @@ -#region Copyright & License Information -/** - * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). - * This file is part of OpenRA Combined Arms, 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.Mods.Common.Traits; -using OpenRA.Traits; - -namespace OpenRA.Mods.CA.Traits -{ - [TraitLocation(SystemActors.Player)] - [Desc("Allows the player to issue the orders the AutoDeployer traits trigger.")] - public class AutoDeployManagerInfo : ConditionalTraitInfo - { - public override object Create(ActorInitializer init) { return new AutoDeployManager(init.Self, this); } - } - - public class AutoDeployManager : ConditionalTrait, IBotTick - { - readonly HashSet> active = new HashSet>(); - readonly HashSet undeployOrders = new HashSet(); - readonly World world; - - public AutoDeployManager(Actor self, AutoDeployManagerInfo info) - : base(info) - { - world = self.World; - } - - public void AddEntry(TraitPair entry) - { - active.Add(entry); - } - - public void AddUndeployOrders(Order order) - { - undeployOrders.Add(order); - } - - void IBotTick.BotTick(IBot bot) - { - foreach (var entry in active) - { - if (entry.Actor.IsDead || !entry.Actor.IsInWorld) - continue; - - if (world.LocalRandom.Next(100) > entry.Trait.Info.DeployChance) - continue; - - var orders = entry.Trait.DeployTraits.Where(d => d.CanIssueDeployOrder(entry.Actor, false)).Select(d => d.IssueDeployOrder(entry.Actor, false)); - - foreach (var order in orders) - bot.QueueOrder(order); - - if (entry.Trait.PrimaryBuilding) - bot.QueueOrder(new Order(AutoDeployer.PrimaryBuildingOrderID, entry.Actor, false)); - } - - active.Clear(); - - foreach (var order in undeployOrders) - { - if (order.Subject.IsDead || !order.Subject.IsInWorld) - continue; - - bot.QueueOrder(order); - } - - undeployOrders.Clear(); - } - } -} diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 25603d5706..21ddcb5aa5 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -536,12 +536,12 @@ U3: VoiceSet: GGIVoice Crushable: RequiresCondition: !invulnerability && !being-warped && undeployed - AutoDeployer@AI: - RequiresCondition: botowner && !deployed && !parachute - DeployChance: 100 - DeployTrigger: Attack - DeployTicks: 5 - UndeployTicks: 50 + IssueOrderToBot: + RequiresCondition: undeployed && !parachute && !reapersnare && !berserk + OrderName: GrantConditionOnDeploy + OrderChance: 100 + OrderTrigger: Attack + OrderInterval: 35 Encyclopedia: Category: Allies/Infantry EncyclopediaExtras: @@ -3208,12 +3208,12 @@ DESO: Sequence: deploy BodyNames: deployed -ReplacedInQueue: - AutoDeployer@AI: - RequiresCondition: botowner && !deployed && !parachute - DeployChance: 20 - DeployTrigger: Attack - DeployTicks: 5 - UndeployTicks: 50 + IssueOrderToBot: + RequiresCondition: undeployed && !parachute && !reapersnare && !berserk + OrderName: GrantConditionOnDeploy + OrderChance: 20 + OrderTrigger: Attack + OrderInterval: 55 GrantConditionOnBotOwner@IAMBOT: Condition: botowner Bots: brutal, vhard, hard, normal, easy, naval, campaign @@ -4194,11 +4194,12 @@ YURI: RequiresCondition: botowner ArmamentNames: aidummy RevokeDelay: 60 - AutoDeployer@AI: - RequiresCondition: aiattack && botowner && !deployed && !parachute - DeployChance: 100 - DeployTrigger: Attack - DeployTicks: 100 + IssueOrderToBot: + RequiresCondition: aiattack && !deployed && !parachute && !reapersnare + OrderName: GrantTimedConditionOnDeploy + OrderChance: 100 + OrderTrigger: Attack + OrderInterval: 31 GrantConditionOnBotOwner@IAMBOT: Condition: botowner Bots: brutal, vhard, hard, normal, easy, naval, campaign diff --git a/mods/ca/rules/player.yaml b/mods/ca/rules/player.yaml index 07926d69b6..9818c812d1 100644 --- a/mods/ca/rules/player.yaml +++ b/mods/ca/rules/player.yaml @@ -378,7 +378,6 @@ Player: TeleportNetworkManager: Type: Wormhole RandomExit: true - AutoDeployManager: CapturedFactionsManager: UpgradesManager: ProductionTracker: diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index 86004fc0bb..fc3f206460 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -8538,6 +8538,12 @@ PTNK: SmartDeploy: True ValidFacings: 0, 256, 512, 768 RequiresCondition: player-france + IssueOrderToBot: + RequiresCondition: undeployed && player-france && !being-warped && !empdisable && !berserk + OrderName: GrantConditionOnDeployTurreted + OrderChance: 100 + OrderTrigger: BecomingIdle, Attack + OrderInterval: 45 WithFacingSpriteBody@Deployed: Sequence: fort RequiresCondition: !undeployed