#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.Graphics; using OpenRA.Mods.Cnc.Activities; using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Orders; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Cnc.Traits { sealed class PortableChronoInfo : PausableConditionalTraitInfo, Requires { [Desc("Cooldown in ticks until the unit can teleport.")] public readonly int ChargeDelay = 500; [Desc("Can the unit teleport only a certain distance?")] public readonly bool HasDistanceLimit = true; [Desc("The maximum distance in cells this unit can teleport (only used if HasDistanceLimit = true).")] public readonly int MaxDistance = 12; [Desc("Sound to play when teleporting.")] public readonly string ChronoshiftSound = "chrotnk1.aud"; [CursorReference] [Desc("Cursor to display when able to deploy the actor.")] public readonly string DeployCursor = "deploy"; [CursorReference] [Desc("Cursor to display when unable to deploy the actor.")] public readonly string DeployBlockedCursor = "deploy-blocked"; [CursorReference] [Desc("Cursor to display when targeting a teleport location.")] public readonly string TargetCursor = "chrono-target"; [CursorReference] [Desc("Cursor to display when the targeted location is blocked.")] public readonly string TargetBlockedCursor = "move-blocked"; [Desc("Kill cargo on teleporting.")] public readonly bool KillCargo = true; [Desc("Flash the screen on teleporting.")] public readonly bool FlashScreen = false; [VoiceReference] public readonly string Voice = "Action"; [Desc("Range circle color.")] public readonly Color CircleColor = Color.FromArgb(128, Color.LawnGreen); [Desc("Range circle line width.")] public readonly float CircleWidth = 1; [Desc("Range circle border color.")] public readonly Color CircleBorderColor = Color.FromArgb(96, Color.Black); [Desc("Range circle border width.")] public readonly float CircleBorderWidth = 3; [Desc("Color to use for the target line.")] public readonly Color TargetLineColor = Color.LawnGreen; public override object Create(ActorInitializer init) { return new PortableChrono(init.Self, this); } } sealed class PortableChrono : PausableConditionalTrait, IIssueOrder, IResolveOrder, ITick, ISelectionBar, IOrderVoice, ISync { readonly IMove move; [Sync] int chargeTick = 0; public PortableChrono(Actor self, PortableChronoInfo info) : base(info) { move = self.Trait(); } void ITick.Tick(Actor self) { if (IsTraitDisabled || IsTraitPaused) return; if (chargeTick > 0) chargeTick--; } public IEnumerable Orders { get { if (IsTraitDisabled) yield break; yield return new PortableChronoOrderTargeter(Info.TargetCursor); yield return new DeployOrderTargeter("PortableChronoDeploy", 5, () => CanTeleport ? Info.DeployCursor : Info.DeployBlockedCursor); } } public Order IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) { if (order.OrderID == "PortableChronoDeploy") { // HACK: Switch the global order generator instead of actually issuing an order if (CanTeleport) self.World.OrderGenerator = new PortableChronoOrderGenerator(self, this); // HACK: We need to issue a fake order to stop the game complaining about the bodge above return new Order(order.OrderID, self, Target.Invalid, queued); } if (order.OrderID == "PortableChronoTeleport") return new Order(order.OrderID, self, target, queued); return null; } public void ResolveOrder(Actor self, Order order) { if (order.OrderString == "PortableChronoTeleport" && order.Target.Type != TargetType.Invalid) { var maxDistance = Info.HasDistanceLimit ? Info.MaxDistance : (int?)null; if (!order.Queued) self.CancelActivity(); var cell = self.World.Map.CellContaining(order.Target.CenterPosition); if (maxDistance != null) self.QueueActivity(move.MoveWithinRange(order.Target, WDist.FromCells(maxDistance.Value), targetLineColor: Info.TargetLineColor)); self.QueueActivity(new Teleport(self, cell, maxDistance, Info.KillCargo, Info.FlashScreen, Info.ChronoshiftSound)); self.QueueActivity(move.MoveTo(cell, 5, targetLineColor: Info.TargetLineColor)); self.ShowTargetLines(); } } string IOrderVoice.VoicePhraseForOrder(Actor self, Order order) { return order.OrderString == "PortableChronoTeleport" ? Info.Voice : null; } public void ResetChargeTime() { chargeTick = Info.ChargeDelay; } public bool CanTeleport => !IsTraitDisabled && !IsTraitPaused && chargeTick <= 0; float ISelectionBar.GetValue() { if (IsTraitDisabled) return 0f; return (float)(Info.ChargeDelay - chargeTick) / Info.ChargeDelay; } Color ISelectionBar.GetColor() { return Color.Magenta; } bool ISelectionBar.DisplayWhenEmpty => false; protected override void TraitDisabled(Actor self) { chargeTick = 0; } } sealed class PortableChronoOrderTargeter : IOrderTargeter { readonly string targetCursor; public PortableChronoOrderTargeter(string targetCursor) { this.targetCursor = targetCursor; } public string OrderID => "PortableChronoTeleport"; public int OrderPriority => 5; public bool IsQueued { get; private set; } public bool TargetOverridesSelection(Actor self, in Target target, List actorsAt, CPos xy, TargetModifiers modifiers) { return true; } public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) { if (modifiers.HasModifier(TargetModifiers.ForceMove)) { var xy = self.World.Map.CellContaining(target.CenterPosition); IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); if (self.IsInWorld && self.Owner.Shroud.IsExplored(xy)) { cursor = targetCursor; return true; } } return false; } } sealed class PortableChronoOrderGenerator : OrderGenerator { readonly Actor self; readonly PortableChrono portableChrono; readonly PortableChronoInfo info; public PortableChronoOrderGenerator(Actor self, PortableChrono portableChrono) { this.self = self; this.portableChrono = portableChrono; info = portableChrono.Info; } protected override IEnumerable OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi) { if (mi.Button == Game.Settings.Game.MouseButtonPreference.Cancel) { world.CancelInputMode(); yield break; } if (self.IsInWorld && self.Location != cell && self.Trait().CanTeleport && self.Owner.Shroud.IsExplored(cell)) { world.CancelInputMode(); yield return new Order("PortableChronoTeleport", self, Target.FromCell(world, cell), mi.Modifiers.HasModifier(Modifiers.Shift)); } } protected override void SelectionChanged(World world, IEnumerable selected) { if (!selected.Contains(self)) world.CancelInputMode(); } protected override void Tick(World world) { if (portableChrono.IsTraitDisabled || portableChrono.IsTraitPaused) { world.CancelInputMode(); } } protected override IEnumerable Render(WorldRenderer wr, World world) { yield break; } protected override IEnumerable RenderAboveShroud(WorldRenderer wr, World world) { yield break; } protected override IEnumerable RenderAnnotations(WorldRenderer wr, World world) { if (!self.IsInWorld || self.Owner != self.World.LocalPlayer) yield break; if (!info.HasDistanceLimit) yield break; yield return new RangeCircleAnnotationRenderable( self.CenterPosition, WDist.FromCells(info.MaxDistance), 0, info.CircleColor, info.CircleWidth, info.CircleBorderColor, info.CircleBorderWidth); } protected override string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi) { if (self.IsInWorld && self.Location != cell && portableChrono.CanTeleport && self.Owner.Shroud.IsExplored(cell)) return info.TargetCursor; else return info.TargetBlockedCursor; } } }