#region Copyright & License Information /* * Copyright 2007-2020 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; using System.Collections.Generic; using System.Linq; using OpenRA.Mods.Common; using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits.Render; using OpenRA.Primitives; using OpenRA.Support; using OpenRA.Traits; namespace OpenRA.Mods.Cnc.Traits { [Desc("Implements the special case handling for the Chronoshiftable return on a construction yard.", "If ReturnOriginalActorOnCondition evaluates true and the actor is not being sold then OriginalActor will be returned to the origin.", "Otherwise, a vortex animation is played and damage is dealt each tick, ignoring modifiers.")] public class ConyardChronoReturnInfo : TraitInfo, Requires, Requires, IObservesVariablesInfo { [SequenceReference] [Desc("Sequence name with the baked-in vortex animation")] 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 BitSet DamageTypes = default(BitSet); [ConsumedConditionReference] [Desc("Boolean expression defining the condition under which to teleport a replacement actor instead of triggering the vortex.")] public readonly BooleanExpression ReturnOriginalActorOnCondition = null; [ActorReference(typeof(MobileInfo))] [Desc("Replacement actor to create when ReturnOriginalActorOnCondition evaluates true.")] 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 override object Create(ActorInitializer init) { return new ConyardChronoReturn(init, this); } } public class ConyardChronoReturn : ITick, ISync, IObservesVariables, ISelectionBar, INotifySold, IDeathActorInitModifier, ITransformActorInitModifier { readonly ConyardChronoReturnInfo info; readonly WithSpriteBody wsb; readonly Health health; readonly Actor self; readonly string faction; int conditionToken = Actor.InvalidConditionToken; Actor chronosphere; int duration; bool returnOriginal; 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.GetValue(info, self.Owner.Faction.InternalName); var returnInit = init.GetOrDefault(info); if (returnInit != null) { returnTicks = returnInit.Ticks; duration = returnInit.Duration; origin = returnInit.Origin; // Defer to the end of tick as the lazy value may reference an actor that hasn't been created yet if (returnInit.Chronosphere != null) init.World.AddFrameEndTask(w => chronosphere = returnInit.Chronosphere.Actor(init.World).Value); } } IEnumerable IObservesVariables.GetVariableObservers() { if (info.ReturnOriginalActorOnCondition != null) yield return new VariableObserver(ReplacementConditionChanged, info.ReturnOriginalActorOnCondition.Variables); } void ReplacementConditionChanged(Actor self, IReadOnlyDictionary conditions) { returnOriginal = info.ReturnOriginalActorOnCondition.Evaluate(conditions); } void TriggerVortex() { if (conditionToken == Actor.InvalidConditionToken) conditionToken = self.GrantCondition(info.Condition); triggered = true; // Don't override the selling animation if (selling) return; wsb.PlayCustomAnimation(self, info.Sequence, () => { triggered = false; if (conditionToken != Actor.InvalidConditionToken) conditionToken = self.RevokeCondition(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(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 (self.WillDispose) return; if (triggered) health.InflictDamage(self, chronosphere, new Damage(info.Damage, info.DamageTypes), true); if (returnTicks <= 0 || --returnTicks > 0) return; if (returnOriginal && !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 ChronoshiftReturnInit(returnTicks, duration, origin, chronosphere)); } void IDeathActorInitModifier.ModifyDeathActorInit(Actor self, TypeDictionary init) { ModifyActorInit(init); } void ITransformActorInitModifier.ModifyTransformActorInit(Actor self, TypeDictionary init) { ModifyActorInit(init); } void INotifySold.Sold(Actor self) { } void INotifySold.Selling(Actor self) { selling = true; } // 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; } } } }