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.
This commit is contained in:
Paul Chote
2018-09-29 16:28:22 +00:00
committed by abcdefg30
parent 588a5d784f
commit a53ef6e503
6 changed files with 237 additions and 86 deletions

View File

@@ -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<CaptureManager>(a, a.TraitOrDefault<CaptureManager>()))
.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<Capturable>();
if (capturable == null)
var captureManager = target.Actor.TraitOrDefault<CaptureManager>();
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<CapturesInfo>());
var externalCapturers = capturers.Except(capturesCapturers).Where(a => a.Info.HasTraitInfo<ExternalCapturesInfo>());
var capturesCapturers = capturers.Where(tp => tp.Actor.Info.HasTraitInfo<CapturesInfo>());
var externalCapturers = capturers.Except(capturesCapturers).Where(tp => tp.Actor.Info.HasTraitInfo<ExternalCapturesInfo>());
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<TTargetType>(Actor capturer, CaptureTarget<TTargetType> target) where TTargetType : class, ITraitInfoInterface

View File

@@ -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<Building>();
captures = self.TraitsImplementing<Captures>().ToArray();
capturable = target.Trait<Capturable>();
health = actor.Trait<IHealth>();
manager = self.Trait<CaptureManager>();
targetManager = target.Trait<CaptureManager>();
}
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,17 +51,28 @@ 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;
// Sabotage instead of capture
if (captures.Info.SabotageThreshold > 0 && !actor.Owner.NonCombatant)
{
var health = actor.Trait<IHealth>();
// 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)
if (100 * (long)health.HP > captures.Info.SabotageThreshold * (long)health.MaxHP)
{
var damage = (int)((long)health.MaxHP * captures.Info.SabotageHPRemoval / 100);
actor.InflictDamage(self, new Damage(damage));
self.Dispose();
return;
}
}
// Do the capture
var oldOwner = actor.Owner;
actor.ChangeOwner(self.Owner);
@@ -73,18 +83,11 @@ namespace OpenRA.Mods.Common.Activities
if (building != null && building.Locked)
building.Unlock();
if (self.Owner.Stances[oldOwner].HasStance(capturesInfo.PlayerExperienceStances))
if (self.Owner.Stances[oldOwner].HasStance(captures.Info.PlayerExperienceStances))
{
var exp = self.Owner.PlayerActor.TraitOrDefault<PlayerExperience>();
if (exp != null)
exp.GiveExperience(capturesInfo.PlayerExperience);
}
}
else
{
// 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));
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);

View File

@@ -927,6 +927,7 @@
<Compile Include="Lint\CheckUnknownTraitFields.cs" />
<Compile Include="Lint\CheckUnknownWeaponFields.cs" />
<Compile Include="Widgets\Logic\PlayerProfileLogic.cs" />
<Compile Include="Traits\CaptureManager.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild">

View File

@@ -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<CaptureManagerInfo>
{
[Desc("CaptureTypes (from the Captures trait) that are able to capture this.")]
public readonly HashSet<string> Types = new HashSet<string>() { "building" };
public readonly BitSet<CaptureType> Types = new BitSet<CaptureType>("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<CapturableInfo>, 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<CaptureManager>();
}
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); }
}
}

View File

@@ -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<CaptureManager> { }
public class CaptureManager : INotifyCreated, INotifyCapture
{
BitSet<CaptureType> allyCapturableTypes;
BitSet<CaptureType> neutralCapturableTypes;
BitSet<CaptureType> enemyCapturableTypes;
BitSet<CaptureType> capturesTypes;
IEnumerable<Capturable> enabledCapturable;
IEnumerable<Captures> enabledCaptures;
public bool BeingCaptured { get; private set; }
void INotifyCreated.Created(Actor self)
{
enabledCapturable = self.TraitsImplementing<Capturable>()
.ToArray()
.Where(Exts.IsTraitEnabled);
enabledCaptures = self.TraitsImplementing<Captures>()
.ToArray()
.Where(Exts.IsTraitEnabled);
RefreshCaptures(self);
RefreshCapturable(self);
}
public void RefreshCapturable(Actor self)
{
allyCapturableTypes = neutralCapturableTypes = enemyCapturableTypes = default(BitSet<CaptureType>);
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<CaptureType>),
(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);
}
}
}

View File

@@ -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<CaptureManagerInfo>
{
[Desc("Types of actors that it can capture, as long as the type also exists in the Capturable Type: trait.")]
public readonly HashSet<string> CaptureTypes = new HashSet<string> { "building" };
public readonly BitSet<CaptureType> CaptureTypes = new BitSet<CaptureType>("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<CapturesInfo>, IIssueOrder, IResolveOrder, IOrderVoice
{
public Captures(CapturesInfo info)
: base(info) { }
readonly CaptureManager captureManager;
public Captures(Actor self, CapturesInfo info)
: base(info)
{
captureManager = self.Trait<CaptureManager>();
}
public IEnumerable<IOrderTargeter> 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<Capturable>();
if (c == null || !c.CanBeTargetedBy(self, target.Owner))
var captureManager = target.TraitOrDefault<CaptureManager>();
if (captureManager == null || !captureManager.CanBeTargetedBy(target, self, captures))
{
cursor = capturesInfo.EnterBlockedCursor;
cursor = captures.Info.EnterBlockedCursor;
return false;
}
cursor = captures.Info.EnterCursor;
if (captures.Info.SabotageThreshold > 0 && !target.Owner.NonCombatant)
{
var health = target.Trait<IHealth>();
// Cast to long to avoid overflow when multiplying by the health
var lowEnoughHealth = health.HP <= (int)(c.Info.CaptureThreshold * (long)health.MaxHP / 100);
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<CapturableInfo>();
if (c == null || !c.CanBeTargetedBy(self, target.Owner))
{
cursor = capturesInfo.EnterCursor;
cursor = captures.Info.EnterCursor;
return false;
}
var health = target.Info.TraitInfoOrDefault<IHealthInfo>();
cursor = captures.Info.EnterCursor;
if (captures.Info.SabotageThreshold > 0 && !target.Owner.NonCombatant)
{
var healthInfo = target.Info.TraitInfoOrDefault<IHealthInfo>();
// 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;
}