Files
OpenRA/OpenRA.Mods.Cnc/Traits/Chronoshiftable.cs

195 lines
6.2 KiB
C#

#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 OpenRA.Mods.Cnc.Activities;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Traits
{
[Desc("Can be teleported via Chronoshift power.")]
public class ChronoshiftableInfo : ConditionalTraitInfo
{
[Desc("Should the actor die instead of being teleported?")]
public readonly bool ExplodeInstead = false;
[Desc("Types of damage that this trait causes to self when 'ExplodeInstead' is true",
"or the return-to-origin is blocked. Leave empty for no damage types.")]
public readonly BitSet<DamageType> DamageTypes = default(BitSet<DamageType>);
public readonly string ChronoshiftSound = "chrono2.aud";
[Desc("Should the actor return to its previous location after the chronoshift wore out?")]
public readonly bool ReturnToOrigin = true;
[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 Chronoshiftable(init, this); }
}
public class Chronoshiftable : ConditionalTrait<ChronoshiftableInfo>, ITick, ISync, ISelectionBar,
IDeathActorInitModifier, ITransformActorInitModifier
{
readonly Actor self;
Actor chronosphere;
bool killCargo;
int duration;
IPositionable iPositionable;
// Return-to-origin logic
[Sync]
public CPos Origin;
[Sync]
public int ReturnTicks = 0;
public Chronoshiftable(ActorInitializer init, ChronoshiftableInfo info)
: base(info)
{
self = init.Self;
var returnInit = init.GetOrDefault<ChronoshiftReturnInit>(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);
}
}
void ITick.Tick(Actor self)
{
if (IsTraitDisabled || !Info.ReturnToOrigin || ReturnTicks <= 0)
return;
// Return to original location
if (--ReturnTicks == 0)
{
// The Move activity is not immediately cancelled, which, combined
// with Activity.Cancel discarding NextActivity without checking the
// IsInterruptable flag, means that a well timed order can cancel the
// Teleport activity queued below - an exploit / cheat of the return mechanic.
// The Teleport activity queued below is guaranteed to either complete
// (force-resetting the actor to the middle of the target cell) or kill
// the actor. It is therefore safe to force-erase the Move activity to
// work around the cancellation bug.
// HACK: this is manipulating private internal actor state
if (self.CurrentActivity is Move)
typeof(Actor).GetProperty("CurrentActivity").SetValue(self, null);
// The actor is killed using Info.DamageTypes if the teleport fails
self.QueueActivity(false, new Teleport(chronosphere ?? self, Origin, null, true, killCargo, Info.ChronoshiftSound,
false, true, Info.DamageTypes));
}
}
protected override void Created(Actor self)
{
iPositionable = self.TraitOrDefault<IPositionable>();
base.Created(self);
}
// Can't be used in synced code, except with ignoreVis.
public virtual bool CanChronoshiftTo(Actor self, CPos targetLocation)
{
// TODO: Allow enemy units to be chronoshifted into bad terrain to kill them
return !IsTraitDisabled && iPositionable != null && iPositionable.CanEnterCell(targetLocation);
}
public virtual bool Teleport(Actor self, CPos targetLocation, int duration, bool killCargo, Actor chronosphere)
{
if (IsTraitDisabled)
return false;
// Some things appear chronoshiftable, but instead they just die.
if (Info.ExplodeInstead)
{
self.World.AddFrameEndTask(w =>
{
// Damage is inflicted by the chronosphere
if (!self.Disposed)
self.Kill(chronosphere, Info.DamageTypes);
});
return true;
}
// Set up return-to-origin info
// If this actor is already counting down to return to
// an existing location then we shouldn't override it
if (ReturnTicks <= 0)
{
Origin = self.Location;
ReturnTicks = duration;
}
this.duration = duration;
this.chronosphere = chronosphere;
this.killCargo = killCargo;
// Set up the teleport
self.QueueActivity(false, new Teleport(chronosphere, targetLocation, null, killCargo, true, Info.ChronoshiftSound));
return true;
}
// Show the remaining time as a bar
float ISelectionBar.GetValue()
{
if (IsTraitDisabled || !Info.ReturnToOrigin)
return 0f;
// 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; } }
void ModifyActorInit(TypeDictionary init)
{
if (IsTraitDisabled || !Info.ReturnToOrigin || 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); }
}
public class ChronoshiftReturnInit : CompositeActorInit
{
public readonly int Ticks;
public readonly int Duration;
public readonly CPos Origin;
public readonly ActorInitActorReference Chronosphere;
public ChronoshiftReturnInit(int ticks, int duration, CPos origin, Actor chronosphere)
{
Ticks = ticks;
Duration = duration;
Origin = origin;
Chronosphere = chronosphere;
}
}
}