diff --git a/OpenRA.Mods.Common/Traits/ProximityCapturable.cs b/OpenRA.Mods.Common/Traits/ProximityCapturable.cs index ca25d8b9de..74ff2d87af 100644 --- a/OpenRA.Mods.Common/Traits/ProximityCapturable.cs +++ b/OpenRA.Mods.Common/Traits/ProximityCapturable.cs @@ -9,187 +9,40 @@ */ #endregion -using System.Collections.Generic; -using System.Linq; -using OpenRA.Mods.Common.Effects; -using OpenRA.Primitives; -using OpenRA.Traits; - namespace OpenRA.Mods.Common.Traits { - [Desc("Actor can be captured by units in a specified proximity.")] - public class ProximityCapturableInfo : TraitInfo, IRulesetLoaded + [Desc("Actor can be captured by units within a certain range.")] + public class ProximityCapturableInfo : ProximityCapturableBaseInfo { - [Desc("Maximum range at which a ProximityCaptor actor can initiate the capture.")] + [Desc("Maximum range at which a " + nameof(ProximityCaptor) + " actor can initiate the capture.")] public readonly WDist Range = WDist.FromCells(5); - [Desc("Allowed ProximityCaptor actors to capture this actor.")] - public readonly BitSet CaptorTypes = new("Player", "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 void RulesetLoaded(Ruleset rules, ActorInfo info) - { - var pci = rules.Actors[SystemActors.Player].TraitInfoOrDefault(); - if (pci == null) - throw new YamlException("ProximityCapturable requires the `Player` actor to have the ProximityCaptor trait."); - } - - public override object Create(ActorInitializer init) { return new ProximityCapturable(init.Self, this); } + public override object Create(ActorInitializer init) { return new ProximityCapturable(init, this); } } - public class ProximityCapturable : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged + public class ProximityCapturable : ProximityCapturableBase { - public readonly Player OriginalOwner; - public bool Captured => Self.Owner != OriginalOwner; + public new readonly ProximityCapturableInfo Info; - public ProximityCapturableInfo Info; - public Actor Self; - - readonly List actorsInRange = new(); - int proximityTrigger; - WPos prevPosition; - bool skipTriggerUpdate; - - public ProximityCapturable(Actor self, ProximityCapturableInfo info) + public ProximityCapturable(ActorInitializer init, ProximityCapturableInfo info) + : base(init, info) { Info = info; - Self = self; - OriginalOwner = self.Owner; } - void INotifyAddedToWorld.AddedToWorld(Actor self) + protected override int CreateTrigger(Actor self) { - if (skipTriggerUpdate) - return; - - // TODO: Eventually support CellTriggers as well - proximityTrigger = self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, WDist.Zero, ActorEntered, ActorLeft); + return self.World.ActorMap.AddProximityTrigger(self.CenterPosition, Info.Range, WDist.Zero, ActorEntered, ActorLeft); } - void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + protected override void RemoveTrigger(Actor self, int trigger) { - if (skipTriggerUpdate) - return; - - self.World.ActorMap.RemoveProximityTrigger(proximityTrigger); - actorsInRange.Clear(); + self.World.ActorMap.RemoveProximityTrigger(trigger); } - void ITick.Tick(Actor self) + protected override void TickInner(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(); - return pc != null && pc.Types.Overlaps(Info.CaptorTypes); - } - - 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 = actorsInRange.All(a => captor.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally); - - // 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, Color.White)); - - var pc = captor.Info.TraitInfoOrDefault(); - foreach (var t in self.TraitsImplementing()) - t.OnCapture(self, captor, previousOwner, captor.Owner, pc.Types); - }); - } - - void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) - { - Game.RunAfterTick(() => skipTriggerUpdate = false); + self.World.ActorMap.UpdateProximityTrigger(trigger, self.CenterPosition, Info.Range, WDist.Zero); } } } diff --git a/OpenRA.Mods.Common/Traits/ProximityCapturableBase.cs b/OpenRA.Mods.Common/Traits/ProximityCapturableBase.cs new file mode 100644 index 0000000000..ebbb19dc5d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/ProximityCapturableBase.cs @@ -0,0 +1,196 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * 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.Linq; +using OpenRA.Mods.Common.Effects; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Actor can be captured by units in a specified proximity.")] + public abstract class ProximityCapturableBaseInfo : TraitInfo, IRulesetLoaded + { + [Desc("Allowed " + nameof(ProximityCaptor) + " actors to capture this actor.")] + public readonly BitSet CaptorTypes = new("Player", "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 `" + nameof(Sticky) + "` behaviour as well.")] + public readonly bool Permanent = false; + + public void RulesetLoaded(Ruleset rules, ActorInfo info) + { + var pci = rules.Actors[SystemActors.Player].TraitInfoOrDefault(); + if (pci == null) + throw new YamlException(nameof(ProximityCapturableBase) + " requires the `" + nameof(Player) + "` actor to have the " + nameof(ProximityCaptor) + " trait."); + } + + public abstract override object Create(ActorInitializer init); + } + + public abstract class ProximityCapturableBase : ITick, INotifyAddedToWorld, INotifyRemovedFromWorld, INotifyOwnerChanged + { + public readonly Player OriginalOwner; + public bool Captured => Self.Owner != OriginalOwner; + + public ProximityCapturableBaseInfo Info; + public Actor Self; + + readonly List actorsInRange = new(); + protected int trigger; + WPos prevPosition; + bool skipTriggerUpdate; + + protected ProximityCapturableBase(ActorInitializer init, ProximityCapturableBaseInfo info) + { + Info = info; + Self = init.Self; + OriginalOwner = Self.Owner; + } + + protected abstract int CreateTrigger(Actor self); + protected abstract void RemoveTrigger(Actor self, int trigger); + protected abstract void TickInner(Actor self); + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + if (skipTriggerUpdate) + return; + + trigger = CreateTrigger(self); + } + + void INotifyRemovedFromWorld.RemovedFromWorld(Actor self) + { + if (skipTriggerUpdate) + return; + + RemoveTrigger(self, trigger); + actorsInRange.Clear(); + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld || self.CenterPosition == prevPosition) + return; + + TickInner(self); + prevPosition = self.CenterPosition; + } + + protected void ActorEntered(Actor other) + { + if (skipTriggerUpdate || !CanBeCapturedBy(other)) + return; + + actorsInRange.Add(other); + UpdateOwnership(); + } + + protected 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(); + return pc != null && pc.Types.Overlaps(Info.CaptorTypes); + } + + void UpdateOwnership() + { + if (Captured && Info.Permanent) + { + // This area has been captured and cannot ever be re-captured, so we get rid of the + // trigger and ensure that it won't be recreated in AddedToWorld. + skipTriggerUpdate = true; + RemoveTrigger(Self, trigger); + + 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 = actorsInRange.All(a => captor.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally); + + // 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, Color.White)); + + var pc = captor.Info.TraitInfoOrDefault(); + foreach (var t in self.TraitsImplementing()) + t.OnCapture(self, captor, previousOwner, captor.Owner, pc.Types); + }); + } + + void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) + { + Game.RunAfterTick(() => skipTriggerUpdate = false); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/RegionProximityCapturable.cs b/OpenRA.Mods.Common/Traits/RegionProximityCapturable.cs new file mode 100644 index 0000000000..98b9bdc03c --- /dev/null +++ b/OpenRA.Mods.Common/Traits/RegionProximityCapturable.cs @@ -0,0 +1,63 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * 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; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Actor can be captured by units entering a certain set of cells.")] + public class RegionProximityCapturableInfo : ProximityCapturableBaseInfo + { + [Desc("Set of cell offsets (relative to the actor's Location) the " + nameof(ProximityCaptor) + " needs to be in to initiate the capture. ", + "A 'Region' ActorInit can be used to override this value per actor. If either is empty or non-existent, ", + "the immediately neighboring cells of the actor will be used.")] + public readonly CVec[] Region = Array.Empty(); + + public override object Create(ActorInitializer init) { return new RegionProximityCapturable(init, this); } + } + + public class RegionProximityCapturable : ProximityCapturableBase + { + readonly CVec[] offsets; + CPos[] region; + + public RegionProximityCapturable(ActorInitializer init, RegionProximityCapturableInfo info) + : base(init, info) + { + offsets = init.GetValue(info, info.Region); + } + + protected override int CreateTrigger(Actor self) + { + region = offsets.Select(o => o + self.Location).ToArray(); + + if (region.Length == 0) + region = Util.ExpandFootprint(new List { self.Location }, true).ToArray(); + + return self.World.ActorMap.AddCellTrigger(region, ActorEntered, ActorLeft); + } + + protected override void RemoveTrigger(Actor self, int trigger) + { + self.World.ActorMap.RemoveCellTrigger(trigger); + } + + protected override void TickInner(Actor self) { } + } + + public class RegionInit : ValueActorInit + { + public RegionInit(CVec[] value) + : base(value) { } + } +}