diff --git a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj
index 0968edf256..762a7a7d93 100644
--- a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj
+++ b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj
@@ -158,6 +158,7 @@
+
diff --git a/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs b/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs
new file mode 100644
index 0000000000..0ea98608d5
--- /dev/null
+++ b/OpenRA.Mods.Cnc/Traits/ConyardChronoReturn.cs
@@ -0,0 +1,264 @@
+#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.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using OpenRA.GameRules;
+using OpenRA.Mods.Common;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Mods.Common.Traits.Render;
+using OpenRA.Primitives;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Cnc.Traits
+{
+ [Desc("Implements the special case handling for the Chronoshiftable return on a construction yard.",
+ "Actors that are actively (un)deploying will be returned to the origin as the original actor.",
+ "Otherwise, a vortex animation is played and damage is dealt each tick, ignoring modifiers.")]
+ public class ConyardChronoReturnInfo : ITraitInfo, Requires, Requires
+ {
+ [Desc("Sequence name with the baked-in vortex animation"), SequenceReference]
+ public readonly string Sequence = "pdox";
+
+ [Desc("Sprite body to play the vortex animation on.")]
+ public readonly string Body = "body";
+
+ [GrantedConditionReference]
+ [Desc("Condition to grant while the vortex animation plays.")]
+ public readonly string Condition = null;
+
+ [Desc("Amount of damage to apply each tick while the vortex animation plays.")]
+ public readonly int Damage = 1000;
+
+ [Desc("Apply the damage using these damagetypes.")]
+ public readonly HashSet DamageTypes = new HashSet();
+
+ [Desc("Actor to transform into when the timer expires during (un)deploy."), ActorReference]
+ public readonly string OriginalActor = "mcv";
+
+ [Desc("Facing of the returned actor.")]
+ public readonly int Facing = 96;
+
+ public readonly string ChronoshiftSound = "chrono2.aud";
+
+ [Desc("The color the bar of the 'return-to-origin' logic has.")]
+ public readonly Color TimeBarColor = Color.White;
+
+ public object Create(ActorInitializer init) { return new ConyardChronoReturn(init, this); }
+ }
+
+ public class ConyardChronoReturn : INotifyCreated, ITick, ISync, ISelectionBar, IDeathActorInitModifier,
+ ITransformActorInitModifier, INotifyBuildComplete, INotifySold, INotifyTransform
+ {
+ readonly ConyardChronoReturnInfo info;
+ readonly WithSpriteBody wsb;
+ readonly Health health;
+ readonly Actor self;
+ readonly string faction;
+
+ ConditionManager conditionManager;
+ int conditionToken = ConditionManager.InvalidConditionToken;
+
+ Actor chronosphere;
+ int duration;
+ bool buildComplete;
+ bool selling;
+
+ [Sync]
+ int returnTicks = 0;
+
+ [Sync]
+ CPos origin;
+
+ [Sync]
+ bool triggered;
+
+ public ConyardChronoReturn(ActorInitializer init, ConyardChronoReturnInfo info)
+ {
+ this.info = info;
+ self = init.Self;
+
+ health = self.Trait();
+
+ wsb = self.TraitsImplementing().Single(w => w.Info.Name == info.Body);
+ faction = init.Contains() ? init.Get() : self.Owner.Faction.InternalName;
+
+ if (init.Contains())
+ returnTicks = init.Get();
+
+ if (init.Contains())
+ duration = init.Get();
+
+ if (init.Contains())
+ origin = init.Get();
+
+ if (init.Contains())
+ chronosphere = init.Get();
+ }
+
+ void INotifyCreated.Created(Actor self)
+ {
+ conditionManager = self.TraitOrDefault();
+ }
+
+ void TriggerVortex()
+ {
+ if (conditionManager != null && !string.IsNullOrEmpty(info.Condition) && conditionToken == ConditionManager.InvalidConditionToken)
+ conditionToken = conditionManager.GrantCondition(self, info.Condition);
+
+ triggered = true;
+
+ // Don't override the selling animation
+ if (selling)
+ return;
+
+ wsb.PlayCustomAnimation(self, info.Sequence, () =>
+ {
+ triggered = false;
+ if (conditionToken != ConditionManager.InvalidConditionToken)
+ conditionToken = conditionManager.RevokeCondition(self, conditionToken);
+ });
+ }
+
+ CPos? ChooseBestDestinationCell(MobileInfo mobileInfo, CPos destination)
+ {
+ if (chronosphere == null)
+ return null;
+
+ if (mobileInfo.CanEnterCell(self.World, null, destination))
+ return destination;
+
+ var max = chronosphere.World.Map.Grid.MaximumTileSearchRange;
+ foreach (var tile in self.World.Map.FindTilesInCircle(destination, max))
+ if (chronosphere.Owner.Shroud.IsExplored(tile) && mobileInfo.CanEnterCell(self.World, null, tile))
+ return tile;
+
+ return null;
+ }
+
+ void ReturnToOrigin()
+ {
+ var selected = self.World.Selection.Contains(self);
+ var controlgroup = self.World.Selection.GetControlGroupForActor(self);
+ var mobileInfo = self.World.Map.Rules.Actors[info.OriginalActor].TraitInfo();
+ var destination = ChooseBestDestinationCell(mobileInfo, origin);
+
+ // Give up if there is no destination
+ // There's not much else we can do.
+ if (destination == null)
+ return;
+
+ foreach (var nt in self.TraitsImplementing())
+ nt.OnTransform(self);
+
+ var init = new TypeDictionary
+ {
+ new LocationInit(destination.Value),
+ new OwnerInit(self.Owner),
+ new FacingInit(info.Facing),
+ new FactionInit(faction),
+ new HealthInit((int)(health.HP * 100L / health.MaxHP))
+ };
+
+ foreach (var modifier in self.TraitsImplementing())
+ modifier.ModifyTransformActorInit(self, init);
+
+ var a = self.World.CreateActor(info.OriginalActor, init);
+ foreach (var nt in self.TraitsImplementing())
+ nt.AfterTransform(a);
+
+ if (selected)
+ self.World.Selection.Add(self.World, a);
+
+ if (controlgroup.HasValue)
+ self.World.Selection.AddToControlGroup(a, controlgroup.Value);
+
+ Game.Sound.Play(SoundType.World, info.ChronoshiftSound, self.World.Map.CenterOfCell(destination.Value));
+ self.Dispose();
+ }
+
+ void ITick.Tick(Actor self)
+ {
+ if (triggered)
+ health.InflictDamage(self, chronosphere, new Damage(info.Damage, info.DamageTypes), true);
+
+ if (returnTicks <= 0 || --returnTicks > 0)
+ return;
+
+ if (!buildComplete && !selling)
+ ReturnToOrigin();
+ else
+ TriggerVortex();
+
+ // Trigger screen desaturate effect
+ foreach (var cpa in self.World.ActorsWithTrait())
+ cpa.Trait.Enable();
+
+ Game.Sound.Play(SoundType.World, info.ChronoshiftSound, self.CenterPosition);
+
+ if (chronosphere != null && self != chronosphere && !chronosphere.Disposed)
+ {
+ var building = chronosphere.TraitOrDefault();
+ if (building != null && building.DefaultAnimation.HasSequence("active"))
+ building.PlayCustomAnimation(chronosphere, "active");
+ }
+ }
+
+ void ModifyActorInit(TypeDictionary init)
+ {
+ if (returnTicks <= 0)
+ return;
+
+ init.Add(new ChronoshiftOriginInit(origin));
+ init.Add(new ChronoshiftReturnInit(returnTicks));
+ init.Add(new ChronoshiftDurationInit(duration));
+ if (chronosphere != self)
+ init.Add(new ChronoshiftChronosphereInit(chronosphere));
+ }
+
+ void IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init) { ModifyActorInit(init); }
+ void ITransformActorInitModifier.ModifyTransformActorInit(Actor self, TypeDictionary init) { ModifyActorInit(init); }
+
+ void INotifyBuildComplete.BuildingComplete(Actor self)
+ {
+ buildComplete = true;
+ }
+
+ void INotifySold.Sold(Actor self) { }
+ void INotifySold.Selling(Actor self)
+ {
+ buildComplete = false;
+ selling = true;
+ }
+
+ void INotifyTransform.BeforeTransform(Actor self)
+ {
+ buildComplete = false;
+ }
+
+ void INotifyTransform.OnTransform(Actor self) { }
+ void INotifyTransform.AfterTransform(Actor self) { }
+
+ // Show the remaining time as a bar
+ float ISelectionBar.GetValue()
+ {
+ // Otherwise an empty bar is rendered all the time
+ if (returnTicks == 0 || !self.Owner.IsAlliedWith(self.World.RenderPlayer))
+ return 0f;
+
+ return (float)returnTicks / duration;
+ }
+
+ Color ISelectionBar.GetColor() { return info.TimeBarColor; }
+ bool ISelectionBar.DisplayWhenEmpty { get { return false; } }
+ }
+}
diff --git a/mods/ra/bits/factpdox.shp b/mods/ra/bits/factpdox.shp
new file mode 100644
index 0000000000..d07b694519
Binary files /dev/null and b/mods/ra/bits/factpdox.shp differ
diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml
index b95a82f742..130895235e 100644
--- a/mods/ra/rules/structures.yaml
+++ b/mods/ra/rules/structures.yaml
@@ -1104,10 +1104,13 @@ FACT:
ActorTypes: e1,e1,e1,tecn,tecn,e6
BaseBuilding:
Transforms:
+ PauseOnCondition: chrono-vortex
IntoActor: mcv
Offset: 1,1
Facing: 96
RequiresCondition: factundeploy
+ Sellable:
+ RequiresCondition: !chrono-vortex
GrantConditionOnPrerequisite@GLOBALFACTUNDEPLOY:
Condition: factundeploy
Prerequisites: global-factundeploy
@@ -1130,6 +1133,9 @@ FACT:
Type: Rectangle
TopLeft: -1536, -1536
BottomRight: 1536, 1536
+ ConyardChronoReturn:
+ Condition: chrono-vortex
+ Damage: 950
PROC:
Inherits: ^Building
diff --git a/mods/ra/sequences/structures.yaml b/mods/ra/sequences/structures.yaml
index e78cd49d4c..63aace949c 100644
--- a/mods/ra/sequences/structures.yaml
+++ b/mods/ra/sequences/structures.yaml
@@ -59,11 +59,16 @@ fact:
build:
Start: 1
Length: 25
+ pdox: factpdox
+ Length: 80
damaged-idle:
Start: 26
damaged-build:
Start: 27
Length: 25
+ damaged-pdox: factpdox
+ Start: 80
+ Length: 80
dead: factdead
Tick: 800
bib: bib2