Files
OpenRA/OpenRA.Mods.Common/Traits/ProximityCapturable.cs
reaperrr b0e90989a6 Add plumbing for customizable vertical ProximityTrigger range
And check DistanceAboveTerrain instead of just vertical distance to upgrade
source.
This is necessary to avoid situations where an actor is technically on the right
vertical distance above/below ground, but not upgraded because it is located on a lower/higher terrain level than the upgrade source.
2016-05-26 22:47:45 +02:00

196 lines
5.6 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2016 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.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
{
[Desc("Actor can be captured by units in a specified proximity.")]
public class ProximityCapturableInfo : ITraitInfo
{
[Desc("Maximum range at which a ProximityCaptor actor can initiate the capture.")]
public readonly WDist Range = WDist.FromCells(5);
[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, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged
{
public readonly Player OriginalOwner;
public bool Captured { get { return Self.Owner != OriginalOwner; } }
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;
Self = self;
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, WDist.Zero, ActorEntered, ActorLeft);
}
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
{
if (skipTriggerUpdate)
return;
self.World.ActorMap.RemoveProximityTrigger(proximityTrigger);
actorsInRange.Clear();
}
public void Tick(Actor self)
{
if (!self.IsInWorld || self.CenterPosition == prevPosition)
return;
self.World.ActorMap.UpdateProximityTrigger(proximityTrigger, self.CenterPosition, Info.Range, WDist.Zero);
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)
{
// 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;
}
// 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)
{
// Unless the Sticky option is set, we revert to the original owner.
if (Captured && !Info.Sticky)
ChangeOwnership(Self, OriginalOwner.PlayerActor);
}
else
{
if (Info.MustBeClear)
{
var isClear = IsClear(Self, captor.Owner);
// 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);
}
}
}
void ChangeOwnership(Actor self, Actor captor)
{
self.World.AddFrameEndTask(w =>
{
if (self.Disposed || captor.Disposed)
return;
// prevent (Added|Removed)FromWorld from firing during Actor.ChangeOwner
skipTriggerUpdate = true;
var previousOwner = self.Owner;
self.ChangeOwner(captor.Owner);
if (self.Owner == self.World.LocalPlayer)
w.Add(new FlashTarget(self));
foreach (var t in self.TraitsImplementing<INotifyCapture>())
t.OnCapture(self, captor, previousOwner, self.Owner);
});
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
skipTriggerUpdate = false;
}
}
}