Improve ProximityCapturable

The following changes are made:

* Replaced the custom actor search with ProximityTriggers.
* Added a `Sticky` flag, which causes the ownership to not revert back to
  the original owner when the captor leaves.
* Added trait documentation.
This commit is contained in:
Oliver Brakmann
2015-11-28 01:30:21 +01:00
parent 3b675c9bad
commit 8b5df6bf2c

View File

@@ -9,8 +9,11 @@
#endregion #endregion
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Linq; using System.Linq;
using OpenRA.Effects; using OpenRA.Effects;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits namespace OpenRA.Mods.Common.Traits
@@ -18,15 +21,26 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Actor can be captured by units in a specified proximity.")] [Desc("Actor can be captured by units in a specified proximity.")]
public class ProximityCapturableInfo : ITraitInfo public class ProximityCapturableInfo : ITraitInfo
{ {
public readonly bool Permanent = false; [Desc("Maximum range at which a ProximityCaptor actor can initiate the capture.")]
public readonly WDist Range = WDist.FromCells(5); public readonly WDist Range = WDist.FromCells(5);
public readonly bool MustBeClear = false;
[Desc("Allowed ProximityCaptor actors to capture this actor.")]
public readonly HashSet<string> CaptorTypes = new HashSet<string> { "Vehicle", "Tank", "Infantry" }; public readonly HashSet<string> CaptorTypes = new HashSet<string> { "Vehicle", "Tank", "Infantry" };
[Desc("If set, the capturing process stops immediately after another player comes into Range.")]
public readonly bool MustBeClear = false;
[Desc("If set, the ownership will not revert back when the captor leaves the area.")]
public readonly bool Sticky = false;
[Desc("If set, the actor can only be captured via this logic once.",
"This option implies the `Sticky` behaviour as well.")]
public readonly bool Permanent = false;
public object Create(ActorInitializer init) { return new ProximityCapturable(init.Self, this); } public object Create(ActorInitializer init) { return new ProximityCapturable(init.Self, this); }
} }
public class ProximityCapturable : ITick public class ProximityCapturable : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged
{ {
public readonly Player OriginalOwner; public readonly Player OriginalOwner;
public bool Captured { get { return Self.Owner != OriginalOwner; } } public bool Captured { get { return Self.Owner != OriginalOwner; } }
@@ -34,6 +48,11 @@ namespace OpenRA.Mods.Common.Traits
public ProximityCapturableInfo Info; public ProximityCapturableInfo Info;
public Actor Self; public Actor Self;
readonly List<Actor> actorsInRange = new List<Actor>();
int proximityTrigger;
WPos prevPosition;
bool skipTriggerUpdate;
public ProximityCapturable(Actor self, ProximityCapturableInfo info) public ProximityCapturable(Actor self, ProximityCapturableInfo info)
{ {
Info = info; Info = info;
@@ -41,59 +60,123 @@ namespace OpenRA.Mods.Common.Traits
OriginalOwner = self.Owner; OriginalOwner = self.Owner;
} }
void INotifyAddedToWorld.AddedToWorld(Actor self)
{
if (skipTriggerUpdate)
return;
// TODO: Eventually support CellTriggers as well
proximityTrigger = self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, ActorEntered, ActorLeft);
}
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
{
if (skipTriggerUpdate)
return;
self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
actorsInRange.Clear();
}
public void Tick(Actor self) public void Tick(Actor self)
{ {
if (Captured && Info.Permanent) return; // Permanent capture if (!self.IsInWorld || self.CenterPosition == prevPosition)
return;
if (!Captured) self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, self.CenterPosition, Info.Range);
prevPosition = self.CenterPosition;
}
void ActorEntered(Actor other)
{
if (skipTriggerUpdate || !CanBeCapturedBy(other))
return;
actorsInRange.Add(other);
UpdateOwnership();
}
void ActorLeft(Actor other)
{
if (skipTriggerUpdate || !CanBeCapturedBy(other))
return;
actorsInRange.Remove(other);
UpdateOwnership();
}
bool CanBeCapturedBy(Actor a)
{
if (a == Self)
return false;
var pc = a.Info.TraitInfoOrDefault<ProximityCaptorInfo>();
return pc != null && pc.Types.Overlaps(Info.CaptorTypes);
}
bool IsClear(Actor self, Player captorOwner)
{
return actorsInRange
.All(a => a.Owner == captorOwner || WorldUtils.AreMutualAllies(a.Owner, captorOwner));
}
void UpdateOwnership()
{
if (Captured && Info.Permanent)
{ {
var captor = GetInRange(self); // This area has been captured and cannot ever be re-captured, so we get rid of the
// ProximityTrigger and ensure that it won't be recreated in AddedToWorld.
if (captor != null) skipTriggerUpdate = true;
if (!Info.MustBeClear || IsClear(self, captor.Owner, OriginalOwner)) Self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
ChangeOwnership(self, captor);
return; return;
} }
// if the area must be clear, and there is more than 1 player nearby => return ownership to default // The actor that has been in the area the longest will be the captor.
if (Info.MustBeClear && !IsClear(self, self.Owner, OriginalOwner)) // The previous implementation used the closest one, but that doesn't work with
// ProximityTriggers since they only generate events when actors enter or leave.
var captor = actorsInRange.FirstOrDefault();
// The last unit left the area
if (captor == null)
{ {
// Revert Ownership // Unless the Sticky option is set, we revert to the original owner.
ChangeOwnership(self, OriginalOwner.PlayerActor); if (Captured && !Info.Sticky)
return; ChangeOwnership(Self, OriginalOwner.PlayerActor);
} }
else
// See if the 'temporary' owner still is in range
if (!IsStillInRange(self))
{ {
// no.. So find a new one if (Info.MustBeClear)
var captor = GetInRange(self);
// got one
if (captor != null)
{ {
ChangeOwnership(self, captor); var isClear = IsClear(Self, captor.Owner);
return;
}
// Revert Ownership otherwise // An enemy unit has wandered into the area, so we've lost control of it.
ChangeOwnership(self, OriginalOwner.PlayerActor); if (Captured && !isClear)
ChangeOwnership(Self, OriginalOwner.PlayerActor);
// We don't own the area yet, but it is clear from enemy units, so we take possession of it.
else if (Self.Owner != captor.Owner && isClear)
ChangeOwnership(Self, captor);
}
else
{
// In all other cases, we just take over.
if (Self.Owner != captor.Owner)
ChangeOwnership(Self, captor);
}
} }
} }
static void ChangeOwnership(Actor self, Actor captor) void ChangeOwnership(Actor self, Actor captor)
{ {
self.World.AddFrameEndTask(w => self.World.AddFrameEndTask(w =>
{ {
if (self.Disposed || captor.Disposed) return; if (self.Disposed || captor.Disposed)
return;
// prevent (Added|Removed)FromWorld from firing during Actor.ChangeOwner
skipTriggerUpdate = true;
var previousOwner = self.Owner; var previousOwner = self.Owner;
self.ChangeOwner(captor.Owner);
// momentarily remove from world so the ownership queries don't get confused
w.Remove(self);
self.Owner = captor.Owner;
w.Add(self);
if (self.Owner == self.World.LocalPlayer) if (self.Owner == self.World.LocalPlayer)
w.Add(new FlashTarget(self)); w.Add(new FlashTarget(self));
@@ -103,47 +186,9 @@ namespace OpenRA.Mods.Common.Traits
}); });
} }
bool CanBeCapturedBy(Actor a) void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{ {
var pc = a.Info.TraitInfoOrDefault<ProximityCaptorInfo>(); skipTriggerUpdate = false;
return pc != null && pc.Types.Overlaps(Info.CaptorTypes);
}
IEnumerable<Actor> UnitsInRange()
{
return Self.World.FindActorsInCircle(Self.CenterPosition, Info.Range)
.Where(a => a.IsInWorld && a != Self && !a.Disposed && !a.Owner.NonCombatant);
}
bool IsClear(Actor self, Player currentOwner, Player originalOwner)
{
return UnitsInRange()
.All(a => a.Owner == originalOwner || a.Owner == currentOwner ||
WorldUtils.AreMutualAllies(a.Owner, currentOwner) || !CanBeCapturedBy(a));
}
// TODO exclude other NeutralActor that aren't permanent
bool IsStillInRange(Actor self)
{
return UnitsInRange().Any(a => a.Owner == self.Owner && CanBeCapturedBy(a));
}
IEnumerable<Actor> CaptorsInRange(Actor self)
{
return UnitsInRange()
.Where(a => a.Owner != OriginalOwner && CanBeCapturedBy(a));
}
// TODO exclude other NeutralActor that aren't permanent
Actor GetInRange(Actor self)
{
return CaptorsInRange(self).ClosestTo(self);
}
int CountPlayersNear(Actor self, Player ignoreMe)
{
return CaptorsInRange(self).Select(a => a.Owner).Where(p => p != ignoreMe)
.Distinct().Count();
} }
} }
} }