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