diff --git a/OpenRA.Mods.CA/Traits/BotModules/SendUnitToAttackBotModule.cs b/OpenRA.Mods.CA/Traits/BotModules/SendUnitToAttackBotModule.cs new file mode 100644 index 0000000000..310be070e7 --- /dev/null +++ b/OpenRA.Mods.CA/Traits/BotModules/SendUnitToAttackBotModule.cs @@ -0,0 +1,196 @@ +#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.Mods.Common.Activities; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Flags] + public enum AttackDistance + { + Closest = 0, + Furthest = 1, + Random = 2 + } + + [TraitLocation(SystemActors.Player)] + [Desc("Bot logic for units that should not be sent with a regular squad, like suicide or subterranean units.")] + public class SendUnitToAttackBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actors used for attack, and their base desire provided for attack desire.", + "When desire reach 100, AI will send them to attack.")] + public readonly Dictionary ActorTypesAndAttackDesire = default; + + [Desc("Target types that can be targeted.")] + public readonly BitSet ValidTargets = new BitSet("Structure"); + + [Desc("Target types that can't be targeted.")] + public readonly BitSet InvalidTargets; + + [Desc("Player relationships that will be targeted.")] + public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Enemy; + + [Desc("Should attack the furthest or closest target. Possible values are Closest, Furthest, Random")] + public readonly AttackDistance AttackDistance = AttackDistance.Closest; + + [Desc("Attack order name.")] + public readonly string AttackOrderName = "Attack"; + + [Desc("Attack location instead of actor.")] + public readonly bool TargetLocation = false; + + [Desc("Find target and try attack target in this interval.")] + public readonly int ScanTick = 463; + + [Desc("The total attack desire increases by this amount per scan", + "Note: When there is no attack unit, the total attack desire will return to 0.")] + public readonly int AttackDesireIncreasedPerScan = 10; + + public override object Create(ActorInitializer init) { return new SendUnitToAttackBotModule(init.Self, this); } + } + + public class SendUnitToAttackBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate isInvalidActor; + int minAssignRoleDelayTicks; + Player targetPlayer; + int desireIncreased; + + public SendUnitToAttackBotModule(Actor self, SendUnitToAttackBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + isInvalidActor = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != targetPlayer; + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || (!a.IsIdle && !(a.CurrentActivity is FlyIdle)); + desireIncreased = 0; + } + + 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); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.ScanTick; + + var attackdesire = 0; + var actors = world.ActorsWithTrait().Select(at => at.Actor).Where(a => + { + if (Info.ActorTypesAndAttackDesire.ContainsKey(a.Info.Name) && !unitCannotBeOrderedOrIsBusy(a)) + { + attackdesire += Info.ActorTypesAndAttackDesire[a.Info.Name]; + return true; + } + + return false; + }).ToList(); + + if (actors.Count == 0) + desireIncreased = 0; + else + desireIncreased += Info.AttackDesireIncreasedPerScan; + + if (desireIncreased + attackdesire < 100) + return; + + // Randomly choose target player to attack + var targetPlayers = world.Players.Where(p => Info.ValidRelationships.HasRelationship(p.RelationshipWith(player)) && p.WinState != WinState.Lost).ToList(); + if (targetPlayers.Count == 0) + return; + + targetPlayer = targetPlayers.Random(world.LocalRandom); + + var targets = world.Actors.Where(a => + { + if (isInvalidActor(a)) + return false; + + var t = a.GetEnabledTargetTypes(); + + if (!Info.ValidTargets.Overlaps(t) || Info.InvalidTargets.Overlaps(t)) + return false; + + var hasModifier = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, player)) + return true; + + hasModifier = true; + } + + return !hasModifier; + }); + + switch (Info.AttackDistance) + { + case AttackDistance.Closest: + targets = targets.OrderBy(a => (a.CenterPosition - actors[0].CenterPosition).HorizontalLengthSquared); + break; + case AttackDistance.Furthest: + targets = targets.OrderByDescending(a => (a.CenterPosition - actors[0].CenterPosition).HorizontalLengthSquared); + break; + case AttackDistance.Random: + targets = targets.Shuffle(world.LocalRandom); + break; + } + + foreach (var t in targets) + { + var orderedActors = new List(); + + foreach (var a in actors) + { + if (!a.Info.HasTraitInfo()) + { + var mobile = a.TraitOrDefault(); + if (mobile == null || !mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, a.Location, t.Location)) + continue; + } + + orderedActors.Add(a); + } + + actors.RemoveAll(a => orderedActors.Contains(a)); + + if (orderedActors.Count > 0) + { + var targeting = Target.Invalid; + if (Info.TargetLocation) + targeting = Target.FromCell(world, t.Location); + else + targeting = Target.FromActor(t); + + bot.QueueOrder(new Order(Info.AttackOrderName, null, targeting, false, groupedActors: orderedActors.ToArray())); + } + + if (actors.Count == 0) + break; + } + } + } + } +} diff --git a/mods/ca/rules/ai.yaml b/mods/ca/rules/ai.yaml index b95941a92b..4b391b9fae 100644 --- a/mods/ca/rules/ai.yaml +++ b/mods/ca/rules/ai.yaml @@ -1861,7 +1861,7 @@ Player: SquadSize: 25 SquadSizeRandomBonus: 11 AirUnitsTypes: heli, harr, pmak, hind, yak, mig, suk, suk.upg, kiro, orca, a10, orcb, auro, apch, venm, rah, scrn, stmr, enrv, mshp - ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk + ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk, dtrk, ttrk, qtnk, spy NavalUnitsTypes: ss,msub,dd,ca,lst,pt,dd2,pt2,ss2,isub,sb,seas NavalProductionTypes: syrd, spen, syrd.gdi, spen.nod ConstructionYardTypes: fact,afac,sfac @@ -1937,6 +1937,7 @@ Player: s3: 40 s4: 15 s6: 5 + spy: 2 u3.squad: 40 rmbc: 15 enli: 10 @@ -2011,6 +2012,9 @@ Player: 4tnk.eradatomic: 40 apoc: 30 apoc.atomic: 30 + dtrk: 1 + ttrk: 1 + qtnk: 1 tpod: 40 rptp: 40 ltnk: 40 @@ -2112,6 +2116,7 @@ Player: e6: 1 n6: 1 s6: 1 + spy: 2 u3.squad: 2 seal: 5 mech: 3 @@ -2144,7 +2149,7 @@ Player: SquadSize: 22 SquadSizeRandomBonus: 9 AirUnitsTypes: heli, harr, pmak, hind, yak, mig, suk, suk.upg, kiro, orca, a10, orcb, auro, apch, venm, rah, scrn, stmr, enrv, mshp - ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk + ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk, dtrk, ttrk, qtnk, spy NavalUnitsTypes: ss,msub,dd,ca,lst,pt,dd2,pt2,ss2,isub,sb,seas NavalProductionTypes: syrd, spen, syrd.gdi, spen.nod ConstructionYardTypes: fact,afac,sfac @@ -2220,6 +2225,7 @@ Player: s3: 40 s4: 15 s6: 5 + spy: 2 u3.squad: 40 rmbc: 15 enli: 10 @@ -2293,6 +2299,9 @@ Player: 4tnk.eradatomic: 40 apoc: 30 apoc.atomic: 30 + dtrk: 1 + ttrk: 1 + qtnk: 1 tpod: 40 rptp: 40 ltnk: 40 @@ -2394,6 +2403,7 @@ Player: e6: 1 n6: 1 s6: 1 + spy: 2 u3.squad: 2 seal: 5 mech: 3 @@ -2426,7 +2436,7 @@ Player: SquadSize: 18 SquadSizeRandomBonus: 8 AirUnitsTypes: heli, harr, pmak, hind, yak, mig, suk, suk.upg, kiro, orca, a10, orcb, auro, apch, venm, rah, scrn, stmr, enrv, mshp - ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk + ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk, dtrk, ttrk, qtnk, spy NavalUnitsTypes: ss,msub,dd,ca,lst,pt,pt2,ss2,dd2,isub,sb,seas ConstructionYardTypes: fact,afac,sfac StaticAntiAirTypes: agun, sam, nsam, cram, shar @@ -2472,6 +2482,7 @@ Player: s3: 40 s4: 15 s6: 5 + spy: 2 u3.squad: 40 rmbc: 15 enli: 10 @@ -2541,6 +2552,9 @@ Player: 4tnk.eradatomic: 40 apoc: 30 apoc.atomic: 30 + dtrk: 1 + ttrk: 1 + qtnk: 1 tpod: 40 rptp: 40 ltnk: 40 @@ -2638,6 +2652,7 @@ Player: e6: 1 n6: 1 s6: 1 + spy: 2 u3.squad: 2 seal: 5 mech: 3 @@ -2670,7 +2685,7 @@ Player: SquadSize: 14 SquadSizeRandomBonus: 5 AirUnitsTypes: heli, harr, pmak, hind, yak, mig, suk, suk.upg, kiro, orca, a10, orcb, auro, apch, venm, rah, scrn, stmr, enrv, mshp - ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk + ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk, dtrk, ttrk, qtnk, spy NavalUnitsTypes: ss,msub,dd,ca,lst,pt,pt2,ss2,dd2,isub,sb,seas ConstructionYardTypes: fact,afac,sfac StaticAntiAirTypes: agun, sam, nsam, cram, shar @@ -2716,6 +2731,7 @@ Player: s3: 40 s4: 15 s6: 5 + spy: 2 u3.squad: 40 rmbc: 15 enli: 10 @@ -2785,6 +2801,9 @@ Player: 4tnk.eradatomic: 40 apoc: 30 apoc.atomic: 30 + dtrk: 1 + ttrk: 1 + qtnk: 1 tpod: 40 rptp: 40 ltnk: 40 @@ -2882,6 +2901,7 @@ Player: e6: 1 n6: 1 s6: 1 + spy: 2 u3.squad: 2 seal: 5 mech: 3 @@ -2911,7 +2931,7 @@ Player: MinimumAttackForceDelay: 25 SquadSize: 1 AirUnitsTypes: heli, harr, pmak, hind, yak, mig, suk, suk.upg, kiro, orca, a10, orcb, auro, apch, venm, rah, scrn, stmr, enrv, mshp - ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk + ExcludeFromSquadsTypes: harv, harv.td, harv.scrin, harv.chrono, mcv, amcv, smcv, dog, e6, n6, s6, badr, badr.bomber, badr.cbomber, badr.nbomber, badr.mbomber, b2b, p51, tran.paradrop, halo.paradrop, nhaw.paradrop, u2, smig, a10.bomber, c17, c17.cargo, c17.clustermines, c17.xo, galx, uav, ocar.reinforce, ocar.xo, ocar.pod, horn, yf23.bomber, pod, pod2, pod3, buzz, buzz.ai, mspk, dtrk, ttrk, qtnk, spy NavalUnitsTypes: ss,msub,dd,ca,lst,pt,pt2,dd2,ss2,isub,sb,seas ConstructionYardTypes: fact,afac,sfac StaticAntiAirTypes: agun, sam, nsam, cram, shar @@ -2944,6 +2964,7 @@ Player: s3: 40 s4: 15 s6: 5 + spy: 2 u3.squad: 40 rmbc: 15 enli: 10 @@ -2994,3 +3015,39 @@ Player: deva: 3 pac: 3 mshp: 1 + SendUnitToAttackBotModule@truck: + RequiresCondition: enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai + ActorTypesAndAttackDesire: + dtrk: 100 + ttrk: 100 + ValidTargets: Structure + InvalidTargets: Water, WaterStructure + AttackDistance: Furthest + SendUnitToAttackBotModule@Mad: + RequiresCondition: enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai + ActorTypesAndAttackDesire: + qtnk: 100 + ValidTargets: Structure + InvalidTargets: Water, WaterStructure + AttackDistance: Closest + AttackOrderName: DetonateAttack + TargetLocation: true + + GrantConditionOnPrerequisite@Disguisespy: + Condition: disguise-first + Prerequisites: disguise.first + SendUnitToAttackBotModule@Disguisespy: + RequiresCondition: (enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai) && disguise-first + ActorTypesAndAttackDesire: + spy: 100 + AttackOrderName: Disguise + ValidTargets: SpyDisguise + AttackDistance: Random + SendUnitToAttackBotModule@Sendspy: + RequiresCondition: (enable-brutal-ai || enable-vhard-ai || enable-hard-ai || enable-normal-ai || enable-easy-ai || enable-naval-ai) && !disguise-first + ActorTypesAndAttackDesire: + spy: 100 + AttackOrderName: Infiltrate + ValidTargets: SpyInfiltrate + InvalidTargets: WaterActor + AttackDistance: Random diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 4ec82ea427..05d7106b8d 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -741,6 +741,9 @@ SPY: TargetTypes: Ground, Infantry Targetable@ChaosImmune: TargetTypes: ChaosImmune + ProvidesPrerequisite@bot-control: + Prerequisite: disguise.first + RequiresCondition: !disguise E7: Inherits: ^Soldier