From a53ef6e50316d3efa928b1f6e57d0cb09f162e36 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sat, 29 Sep 2018 16:28:22 +0000 Subject: [PATCH] Add CaptureManager trait to fix multiple-trait interactions. This fixes the various edge cases that occur when multiple Captures or Capturable traits are defined on an actor and are toggled using conditions. The Sabotage threshold field moves from Capturable to Captures in order to simplify the plumbing. The previous behaviour ingame can be restored by creating a new capturable type for each threshold level, each with their own Captures trait. --- OpenRA.Mods.Common/AI/HackyAI.cs | 27 ++-- OpenRA.Mods.Common/Activities/CaptureActor.cs | 71 +++++----- OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 1 + OpenRA.Mods.Common/Traits/Capturable.cs | 26 ++-- OpenRA.Mods.Common/Traits/CaptureManager.cs | 125 ++++++++++++++++++ OpenRA.Mods.Common/Traits/Captures.cs | 73 ++++++---- 6 files changed, 237 insertions(+), 86 deletions(-) create mode 100644 OpenRA.Mods.Common/Traits/CaptureManager.cs 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; }