Merge pull request #10526 from obrakmann/koth-refactor
Improve ProximityCapturable
This commit is contained in:
@@ -9,8 +9,11 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.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.")]
|
||||
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 bool MustBeClear = false;
|
||||
|
||||
[Desc("Allowed ProximityCaptor actors to capture this actor.")]
|
||||
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 class ProximityCapturable : ITick
|
||||
public class ProximityCapturable : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged
|
||||
{
|
||||
public readonly Player OriginalOwner;
|
||||
public bool Captured { get { return Self.Owner != OriginalOwner; } }
|
||||
@@ -34,6 +48,11 @@ namespace OpenRA.Mods.Common.Traits
|
||||
public ProximityCapturableInfo Info;
|
||||
public Actor Self;
|
||||
|
||||
readonly List<Actor> actorsInRange = new List<Actor>();
|
||||
int proximityTrigger;
|
||||
WPos prevPosition;
|
||||
bool skipTriggerUpdate;
|
||||
|
||||
public ProximityCapturable(Actor self, ProximityCapturableInfo info)
|
||||
{
|
||||
Info = info;
|
||||
@@ -41,59 +60,123 @@ namespace OpenRA.Mods.Common.Traits
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
if (captor != null)
|
||||
if (!Info.MustBeClear || IsClear(self, captor.Owner, OriginalOwner))
|
||||
ChangeOwnership(self, captor);
|
||||
|
||||
// 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.
|
||||
skipTriggerUpdate = true;
|
||||
Self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
|
||||
return;
|
||||
}
|
||||
|
||||
// if the area must be clear, and there is more than 1 player nearby => return ownership to default
|
||||
if (Info.MustBeClear && !IsClear(self, self.Owner, OriginalOwner))
|
||||
// The actor that has been in the area the longest will be the captor.
|
||||
// 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
|
||||
ChangeOwnership(self, OriginalOwner.PlayerActor);
|
||||
return;
|
||||
// Unless the Sticky option is set, we revert to the original owner.
|
||||
if (Captured && !Info.Sticky)
|
||||
ChangeOwnership(Self, OriginalOwner.PlayerActor);
|
||||
}
|
||||
|
||||
// See if the 'temporary' owner still is in range
|
||||
if (!IsStillInRange(self))
|
||||
else
|
||||
{
|
||||
// no.. So find a new one
|
||||
var captor = GetInRange(self);
|
||||
|
||||
// got one
|
||||
if (captor != null)
|
||||
if (Info.MustBeClear)
|
||||
{
|
||||
ChangeOwnership(self, captor);
|
||||
return;
|
||||
}
|
||||
var isClear = IsClear(Self, captor.Owner);
|
||||
|
||||
// Revert Ownership otherwise
|
||||
ChangeOwnership(self, OriginalOwner.PlayerActor);
|
||||
// An enemy unit has wandered into the area, so we've lost control of it.
|
||||
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 =>
|
||||
{
|
||||
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;
|
||||
|
||||
// momentarily remove from world so the ownership queries don't get confused
|
||||
w.Remove(self);
|
||||
self.Owner = captor.Owner;
|
||||
w.Add(self);
|
||||
self.ChangeOwner(captor.Owner);
|
||||
|
||||
if (self.Owner == self.World.LocalPlayer)
|
||||
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>();
|
||||
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();
|
||||
skipTriggerUpdate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
public enum RangeCircleVisibility { Always, WhenSelected }
|
||||
|
||||
[Desc("Renders an arbitrary circle when selected or placing a structure")]
|
||||
class WithRangeCircleInfo : ITraitInfo, IPlaceBuildingDecorationInfo
|
||||
{
|
||||
@@ -25,6 +27,16 @@ namespace OpenRA.Mods.Common.Traits
|
||||
[Desc("Color of the circle")]
|
||||
public readonly Color Color = Color.FromArgb(128, Color.White);
|
||||
|
||||
[Desc("If set, the color of the owning player will be used instead of `Color`.")]
|
||||
public readonly bool UsePlayerColor = false;
|
||||
|
||||
[Desc("Stances of players which will be able to see the circle.",
|
||||
"Valid values are combinations of `None`, `Ally`, `Enemy` and `Neutral`.")]
|
||||
public readonly Stance ValidStances = Stance.Ally;
|
||||
|
||||
[Desc("When to show the range circle. Valid values are `Always`, and `WhenSelected`")]
|
||||
public readonly RangeCircleVisibility Visible = RangeCircleVisibility.WhenSelected;
|
||||
|
||||
[Desc("Range of the circle")]
|
||||
public readonly WDist Range = WDist.Zero;
|
||||
|
||||
@@ -38,15 +50,15 @@ namespace OpenRA.Mods.Common.Traits
|
||||
Color.FromArgb(96, Color.Black));
|
||||
|
||||
foreach (var a in w.ActorsWithTrait<WithRangeCircle>())
|
||||
if (a.Actor.Owner.IsAlliedWith(w.RenderPlayer) && a.Trait.Info.Type == Type)
|
||||
foreach (var r in a.Trait.RenderAfterWorld(wr))
|
||||
if (a.Trait.Info.Type == Type)
|
||||
foreach (var r in a.Trait.RenderRangeCircle(wr))
|
||||
yield return r;
|
||||
}
|
||||
|
||||
public object Create(ActorInitializer init) { return new WithRangeCircle(init.Self, this); }
|
||||
}
|
||||
|
||||
class WithRangeCircle : IPostRenderSelection
|
||||
class WithRangeCircle : IPostRenderSelection, IPostRender
|
||||
{
|
||||
public readonly WithRangeCircleInfo Info;
|
||||
readonly Actor self;
|
||||
@@ -57,17 +69,44 @@ namespace OpenRA.Mods.Common.Traits
|
||||
Info = info;
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> RenderAfterWorld(WorldRenderer wr)
|
||||
bool Visible
|
||||
{
|
||||
if (!self.Owner.IsAlliedWith(self.World.RenderPlayer))
|
||||
yield break;
|
||||
get
|
||||
{
|
||||
var p = self.World.RenderPlayer;
|
||||
return p == null || Info.ValidStances.HasStance(self.Owner.Stances[p]) || (p.Spectating && !p.NonCombatant);
|
||||
}
|
||||
}
|
||||
|
||||
yield return new RangeCircleRenderable(
|
||||
self.CenterPosition,
|
||||
Info.Range,
|
||||
0,
|
||||
Info.Color,
|
||||
Color.FromArgb(96, Color.Black));
|
||||
public IEnumerable<IRenderable> RenderRangeCircle(WorldRenderer wr)
|
||||
{
|
||||
if (Info.Visible == RangeCircleVisibility.WhenSelected && Visible)
|
||||
yield return new RangeCircleRenderable(
|
||||
self.CenterPosition,
|
||||
Info.Range,
|
||||
0,
|
||||
Info.UsePlayerColor ? self.Owner.Color.RGB : Info.Color,
|
||||
Color.FromArgb(96, Color.Black));
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
IEnumerable<IRenderable> IPostRenderSelection.RenderAfterWorld(WorldRenderer wr)
|
||||
{
|
||||
return RenderRangeCircle(wr);
|
||||
}
|
||||
|
||||
void IPostRender.RenderAfterWorld(WorldRenderer wr, Actor self)
|
||||
{
|
||||
if (Info.Visible == RangeCircleVisibility.Always && Visible)
|
||||
RangeCircleRenderable.DrawRangeCircle(
|
||||
wr,
|
||||
self.CenterPosition,
|
||||
Info.Range,
|
||||
1,
|
||||
Info.UsePlayerColor ? self.Owner.Color.RGB : Info.Color,
|
||||
3,
|
||||
Color.FromArgb(96, Color.Black));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user