diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index f74307f683..e7ebfc0623 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -656,7 +656,12 @@ namespace OpenRA.Mods.Common.AI if (!Info.CapturingActorTypes.Any() || Player.WinState != WinState.Undefined) return; - var capturers = unitsHangingAroundTheBase.Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name)).ToArray(); + var capturers = unitsHangingAroundTheBase + .Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name)) + .Select(a => new TraitPair(a, a.TraitOrDefault())) + .Where(tp => tp.Trait != null) + .ToArray(); + if (capturers.Length == 0) return; @@ -674,11 +679,11 @@ namespace OpenRA.Mods.Common.AI if (target.Info == null) return false; - var capturable = target.Actor.TraitOrDefault(); - if (capturable == null) + var captureManager = target.Actor.TraitOrDefault(); + if (captureManager == null) return false; - return capturers.Any(capturer => capturable.CanBeTargetedBy(capturer, target.Actor.Owner)); + return capturers.Any(tp => captureManager.CanBeTargetedBy(target.Actor, tp.Actor, tp.Trait)); }) .OrderByDescending(target => target.Actor.GetSellValue()) .Take(maximumCaptureTargetOptions); @@ -694,7 +699,7 @@ namespace OpenRA.Mods.Common.AI if (externalCapturable == null) return false; - return capturers.Any(capturer => externalCapturable.CanBeTargetedBy(capturer, target.Actor.Owner)); + return capturers.Any(tp => externalCapturable.CanBeTargetedBy(tp.Actor, target.Actor.Owner)); }) .OrderByDescending(target => target.Actor.GetSellValue()) .Take(maximumCaptureTargetOptions); @@ -708,14 +713,14 @@ namespace OpenRA.Mods.Common.AI if (!capturableTargetOptions.Any() && !externalCapturableTargetOptions.Any()) return; - var capturesCapturers = capturers.Where(a => a.Info.HasTraitInfo()); - var externalCapturers = capturers.Except(capturesCapturers).Where(a => a.Info.HasTraitInfo()); + var capturesCapturers = capturers.Where(tp => tp.Actor.Info.HasTraitInfo()); + var externalCapturers = capturers.Except(capturesCapturers).Where(tp => tp.Actor.Info.HasTraitInfo()); - foreach (var capturer in capturesCapturers) - QueueCaptureOrderFor(capturer, GetCapturerTargetClosestToOrDefault(capturer, capturableTargetOptions)); + foreach (var tp in capturesCapturers) + QueueCaptureOrderFor(tp.Actor, GetCapturerTargetClosestToOrDefault(tp.Actor, capturableTargetOptions)); - foreach (var capturer in externalCapturers) - QueueCaptureOrderFor(capturer, GetCapturerTargetClosestToOrDefault(capturer, externalCapturableTargetOptions)); + foreach (var tp in externalCapturers) + QueueCaptureOrderFor(tp.Actor, GetCapturerTargetClosestToOrDefault(tp.Actor, externalCapturableTargetOptions)); } void QueueCaptureOrderFor(Actor capturer, CaptureTarget target) where TTargetType : class, ITraitInfoInterface diff --git a/OpenRA.Mods.Common/Activities/CaptureActor.cs b/OpenRA.Mods.Common/Activities/CaptureActor.cs index 0d24ff9bea..3f1cc78d4a 100644 --- a/OpenRA.Mods.Common/Activities/CaptureActor.cs +++ b/OpenRA.Mods.Common/Activities/CaptureActor.cs @@ -9,6 +9,7 @@ */ #endregion +using System; using System.Linq; using OpenRA.Activities; using OpenRA.Mods.Common.Traits; @@ -20,28 +21,26 @@ namespace OpenRA.Mods.Common.Activities { readonly Actor actor; readonly Building building; - readonly Capturable capturable; - readonly Captures[] captures; - readonly IHealth health; + readonly CaptureManager targetManager; + readonly CaptureManager manager; public CaptureActor(Actor self, Actor target) : base(self, target, EnterBehaviour.Dispose) { actor = target; building = actor.TraitOrDefault(); - captures = self.TraitsImplementing().ToArray(); - capturable = target.Trait(); - health = actor.Trait(); + manager = self.Trait(); + targetManager = target.Trait(); } protected override bool CanReserve(Actor self) { - return !capturable.BeingCaptured && capturable.CanBeTargetedBy(self, actor.Owner); + return !actor.IsDead && !targetManager.BeingCaptured && targetManager.CanBeTargetedBy(actor, self, manager); } protected override void OnInside(Actor self) { - if (actor.IsDead || capturable.BeingCaptured || capturable.IsTraitDisabled) + if (!CanReserve(self)) return; if (building != null && !building.Lock()) @@ -52,39 +51,43 @@ namespace OpenRA.Mods.Common.Activities if (building != null && building.Locked) building.Unlock(); - var activeCaptures = captures.FirstOrDefault(c => !c.IsTraitDisabled); - - if (actor.IsDead || capturable.BeingCaptured || activeCaptures == null) + // Prioritize capturing over sabotaging + var captures = manager.ValidCapturesWithLowestSabotageThreshold(self, actor, targetManager); + if (captures == null) return; - var capturesInfo = activeCaptures.Info; - - // Cast to long to avoid overflow when multiplying by the health - var lowEnoughHealth = health.HP <= (int)(capturable.Info.CaptureThreshold * (long)health.MaxHP / 100); - if (!capturesInfo.Sabotage || lowEnoughHealth || actor.Owner.NonCombatant) + // Sabotage instead of capture + if (captures.Info.SabotageThreshold > 0 && !actor.Owner.NonCombatant) { - var oldOwner = actor.Owner; + var health = actor.Trait(); - actor.ChangeOwner(self.Owner); - - foreach (var t in actor.TraitsImplementing()) - t.OnCapture(actor, self, oldOwner, self.Owner); - - if (building != null && building.Locked) - building.Unlock(); - - if (self.Owner.Stances[oldOwner].HasStance(capturesInfo.PlayerExperienceStances)) + // Cast to long to avoid overflow when multiplying by the health + if (100 * (long)health.HP > captures.Info.SabotageThreshold * (long)health.MaxHP) { - var exp = self.Owner.PlayerActor.TraitOrDefault(); - if (exp != null) - exp.GiveExperience(capturesInfo.PlayerExperience); + var damage = (int)((long)health.MaxHP * captures.Info.SabotageHPRemoval / 100); + actor.InflictDamage(self, new Damage(damage)); + + self.Dispose(); + return; } } - else + + // Do the capture + var oldOwner = actor.Owner; + + actor.ChangeOwner(self.Owner); + + foreach (var t in actor.TraitsImplementing()) + t.OnCapture(actor, self, oldOwner, self.Owner); + + if (building != null && building.Locked) + building.Unlock(); + + if (self.Owner.Stances[oldOwner].HasStance(captures.Info.PlayerExperienceStances)) { - // Cast to long to avoid overflow when multiplying by the health - var damage = (int)((long)health.MaxHP * capturesInfo.SabotageHPRemoval / 100); - actor.InflictDamage(self, new Damage(damage)); + var exp = self.Owner.PlayerActor.TraitOrDefault(); + if (exp != null) + exp.GiveExperience(captures.Info.PlayerExperience); } self.Dispose(); @@ -93,7 +96,7 @@ namespace OpenRA.Mods.Common.Activities public override Activity Tick(Actor self) { - if (captures.All(c => c.IsTraitDisabled)) + if (!targetManager.CanBeTargetedBy(actor, self, manager)) Cancel(self); return base.Tick(self); diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 99137a0dc0..e3ba2cf9bc 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -927,6 +927,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/Capturable.cs b/OpenRA.Mods.Common/Traits/Capturable.cs index 96bba0cdc7..27b4be205e 100644 --- a/OpenRA.Mods.Common/Traits/Capturable.cs +++ b/OpenRA.Mods.Common/Traits/Capturable.cs @@ -9,25 +9,23 @@ */ #endregion -using System.Collections.Generic; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("This actor can be captured by a unit with Captures: trait.")] - public class CapturableInfo : ConditionalTraitInfo + public class CapturableInfo : ConditionalTraitInfo, Requires { [Desc("CaptureTypes (from the Captures trait) that are able to capture this.")] - public readonly HashSet Types = new HashSet() { "building" }; + public readonly BitSet Types = new BitSet("building"); [Desc("What diplomatic stances can be captured by this actor.")] public readonly Stance ValidStances = Stance.Neutral | Stance.Enemy; - [Desc("Health percentage the target must be at (or below) before it can be captured.")] - public readonly int CaptureThreshold = 50; public readonly bool CancelActivity = false; - public override object Create(ActorInitializer init) { return new Capturable(this); } + public override object Create(ActorInitializer init) { return new Capturable(init.Self, this); } public bool CanBeTargetedBy(Actor captor, Player owner) { @@ -48,15 +46,16 @@ namespace OpenRA.Mods.Common.Traits public class Capturable : ConditionalTrait, INotifyCapture { - public bool BeingCaptured { get; private set; } - public Capturable(CapturableInfo info) - : base(info) { } + readonly CaptureManager captureManager; + + public Capturable(Actor self, CapturableInfo info) + : base(info) + { + captureManager = self.Trait(); + } public void OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner) { - BeingCaptured = true; - self.World.AddFrameEndTask(w => BeingCaptured = false); - if (Info.CancelActivity) { var stop = new Order("Stop", self, false); @@ -72,5 +71,8 @@ namespace OpenRA.Mods.Common.Traits return Info.CanBeTargetedBy(captor, owner); } + + protected override void TraitEnabled(Actor self) { captureManager.RefreshCapturable(self); } + protected override void TraitDisabled(Actor self) { captureManager.RefreshCapturable(self); } } } diff --git a/OpenRA.Mods.Common/Traits/CaptureManager.cs b/OpenRA.Mods.Common/Traits/CaptureManager.cs new file mode 100644 index 0000000000..d0aa5ddf4a --- /dev/null +++ b/OpenRA.Mods.Common/Traits/CaptureManager.cs @@ -0,0 +1,125 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + public sealed class CaptureType { CaptureType() { } } + + [Desc("Manages Captures and Capturable traits on an actor.")] + public class CaptureManagerInfo : TraitInfo { } + + public class CaptureManager : INotifyCreated, INotifyCapture + { + BitSet allyCapturableTypes; + BitSet neutralCapturableTypes; + BitSet enemyCapturableTypes; + BitSet capturesTypes; + + IEnumerable enabledCapturable; + IEnumerable enabledCaptures; + + public bool BeingCaptured { get; private set; } + + void INotifyCreated.Created(Actor self) + { + enabledCapturable = self.TraitsImplementing() + .ToArray() + .Where(Exts.IsTraitEnabled); + + enabledCaptures = self.TraitsImplementing() + .ToArray() + .Where(Exts.IsTraitEnabled); + + RefreshCaptures(self); + RefreshCapturable(self); + } + + public void RefreshCapturable(Actor self) + { + allyCapturableTypes = neutralCapturableTypes = enemyCapturableTypes = default(BitSet); + foreach (var c in enabledCapturable) + { + if (c.Info.ValidStances.HasStance(Stance.Ally)) + allyCapturableTypes = allyCapturableTypes.Union(c.Info.Types); + + if (c.Info.ValidStances.HasStance(Stance.Neutral)) + neutralCapturableTypes = neutralCapturableTypes.Union(c.Info.Types); + + if (c.Info.ValidStances.HasStance(Stance.Enemy)) + enemyCapturableTypes = enemyCapturableTypes.Union(c.Info.Types); + } + } + + public void RefreshCaptures(Actor self) + { + capturesTypes = enabledCaptures.Aggregate( + default(BitSet), + (a, b) => a.Union(b.Info.CaptureTypes)); + } + + public bool CanBeTargetedBy(Actor self, Actor captor, CaptureManager captorManager) + { + var stance = self.Owner.Stances[captor.Owner]; + if (stance.HasStance(Stance.Enemy)) + return captorManager.capturesTypes.Overlaps(enemyCapturableTypes); + + if (stance.HasStance(Stance.Neutral)) + return captorManager.capturesTypes.Overlaps(neutralCapturableTypes); + + if (stance.HasStance(Stance.Ally)) + return captorManager.capturesTypes.Overlaps(allyCapturableTypes); + + return false; + } + + public bool CanBeTargetedBy(Actor self, Actor captor, Captures captures) + { + if (captures.IsTraitDisabled) + return false; + + var stance = self.Owner.Stances[captor.Owner]; + if (stance.HasStance(Stance.Enemy)) + return captures.Info.CaptureTypes.Overlaps(enemyCapturableTypes); + + if (stance.HasStance(Stance.Neutral)) + return captures.Info.CaptureTypes.Overlaps(neutralCapturableTypes); + + if (stance.HasStance(Stance.Ally)) + return captures.Info.CaptureTypes.Overlaps(allyCapturableTypes); + + return false; + } + + public Captures ValidCapturesWithLowestSabotageThreshold(Actor self, Actor captee, CaptureManager capteeManager) + { + if (captee.IsDead) + return null; + + foreach (var c in enabledCaptures.OrderBy(c => c.Info.SabotageThreshold)) + if (capteeManager.CanBeTargetedBy(captee, self, c)) + return c; + + return null; + } + + public void OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner) + { + BeingCaptured = true; + self.World.AddFrameEndTask(w => BeingCaptured = false); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Captures.cs b/OpenRA.Mods.Common/Traits/Captures.cs index 0eb8ad7190..c8bc5c786a 100644 --- a/OpenRA.Mods.Common/Traits/Captures.cs +++ b/OpenRA.Mods.Common/Traits/Captures.cs @@ -11,22 +11,25 @@ using System.Collections.Generic; using System.Drawing; +using System.Linq; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Orders; +using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("This actor can capture other actors which have the Capturable: trait.")] - public class CapturesInfo : ConditionalTraitInfo + public class CapturesInfo : ConditionalTraitInfo, Requires { [Desc("Types of actors that it can capture, as long as the type also exists in the Capturable Type: trait.")] - public readonly HashSet CaptureTypes = new HashSet { "building" }; + public readonly BitSet CaptureTypes = new BitSet("building"); - [Desc("Unit will do damage to the actor instead of capturing it. Unit is destroyed when sabotaging.")] - public readonly bool Sabotage = true; + [Desc("Targets with health above this percentage will be sabotaged instead of captured.", + "Set to 0 to disable sabotaging.")] + public readonly int SabotageThreshold = 50; - [Desc("Only used if Sabotage=true. Sabotage damage expressed as a percentage of enemy health removed.")] + [Desc("Sabotage damage expressed as a percentage of maximum target health.")] public readonly int SabotageHPRemoval = 50; [Desc("Experience granted to the capturing player.")] @@ -41,13 +44,18 @@ namespace OpenRA.Mods.Common.Traits [VoiceReference] public readonly string Voice = "Action"; - public override object Create(ActorInitializer init) { return new Captures(this); } + public override object Create(ActorInitializer init) { return new Captures(init.Self, this); } } public class Captures : ConditionalTrait, IIssueOrder, IResolveOrder, IOrderVoice { - public Captures(CapturesInfo info) - : base(info) { } + readonly CaptureManager captureManager; + + public Captures(Actor self, CapturesInfo info) + : base(info) + { + captureManager = self.Trait(); + } public IEnumerable Orders { @@ -56,7 +64,7 @@ namespace OpenRA.Mods.Common.Traits if (IsTraitDisabled) yield break; - yield return new CaptureOrderTargeter(Info); + yield return new CaptureOrderTargeter(this); } } @@ -89,54 +97,61 @@ namespace OpenRA.Mods.Common.Traits self.QueueActivity(new CaptureActor(self, target.Actor)); } + protected override void TraitEnabled(Actor self) { captureManager.RefreshCaptures(self); } + protected override void TraitDisabled(Actor self) { captureManager.RefreshCaptures(self); } + class CaptureOrderTargeter : UnitOrderTargeter { - readonly CapturesInfo capturesInfo; + readonly Captures captures; - public CaptureOrderTargeter(CapturesInfo info) - : base("CaptureActor", 6, info.EnterCursor, true, true) + public CaptureOrderTargeter(Captures captures) + : base("CaptureActor", 6, captures.Info.EnterCursor, true, true) { - capturesInfo = info; + this.captures = captures; } public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) { - var c = target.TraitOrDefault(); - if (c == null || !c.CanBeTargetedBy(self, target.Owner)) + var captureManager = target.TraitOrDefault(); + if (captureManager == null || !captureManager.CanBeTargetedBy(target, self, captures)) { - cursor = capturesInfo.EnterBlockedCursor; + cursor = captures.Info.EnterBlockedCursor; return false; } - var health = target.Trait(); + cursor = captures.Info.EnterCursor; - // Cast to long to avoid overflow when multiplying by the health - var lowEnoughHealth = health.HP <= (int)(c.Info.CaptureThreshold * (long)health.MaxHP / 100); + if (captures.Info.SabotageThreshold > 0 && !target.Owner.NonCombatant) + { + var health = target.Trait(); - cursor = !capturesInfo.Sabotage || lowEnoughHealth || target.Owner.NonCombatant - ? capturesInfo.EnterCursor : capturesInfo.SabotageCursor; + // Sabotage instead of capture + if ((long)health.HP * 100 > captures.Info.SabotageThreshold * (long)health.MaxHP) + cursor = captures.Info.SabotageCursor; + } return true; } public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) { - // TODO: This doesn't account for disabled traits. // Actors with FrozenUnderFog should not disable the Capturable trait. var c = target.Info.TraitInfoOrDefault(); if (c == null || !c.CanBeTargetedBy(self, target.Owner)) { - cursor = capturesInfo.EnterCursor; + cursor = captures.Info.EnterCursor; return false; } - var health = target.Info.TraitInfoOrDefault(); + cursor = captures.Info.EnterCursor; + if (captures.Info.SabotageThreshold > 0 && !target.Owner.NonCombatant) + { + var healthInfo = target.Info.TraitInfoOrDefault(); - // Cast to long to avoid overflow when multiplying by the health - var lowEnoughHealth = target.HP <= (int)(c.CaptureThreshold * (long)health.MaxHP / 100); - - cursor = !capturesInfo.Sabotage || lowEnoughHealth || target.Owner.NonCombatant - ? capturesInfo.EnterCursor : capturesInfo.SabotageCursor; + // Sabotage instead of capture + if ((long)target.HP * 100 > captures.Info.SabotageThreshold * (long)healthInfo.MaxHP) + cursor = captures.Info.SabotageCursor; + } return true; }