From 6ced7b274c767be16623e609cce41d4ae0f8a0c6 Mon Sep 17 00:00:00 2001 From: Curtis Shmyr Date: Sat, 22 Mar 2014 00:04:29 -0600 Subject: [PATCH] Chronotank teleporation by holding SHIFT or ALT and issuing a move order --- CHANGELOG | 3 + OpenRA.Game/WorldUtils.cs | 2 +- OpenRA.Mods.RA/Activities/Teleport.cs | 67 +++++- OpenRA.Mods.RA/ChronoshiftDeploy.cs | 163 -------------- OpenRA.Mods.RA/Chronoshiftable.cs | 6 +- OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 3 +- .../Orders/SetChronoTankDestination.cs | 56 ----- OpenRA.Mods.RA/PortableChrono.cs | 201 ++++++++++++++++++ OpenRA.Utility/UpgradeRules.cs | 10 + mods/ra/rules/vehicles.yaml | 2 +- 10 files changed, 280 insertions(+), 233 deletions(-) delete mode 100644 OpenRA.Mods.RA/ChronoshiftDeploy.cs delete mode 100644 OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs create mode 100644 OpenRA.Mods.RA/PortableChrono.cs diff --git a/CHANGELOG b/CHANGELOG index ab481d73c9..c7ef319749 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ NEW: Added the submarine detection ability to Submarines and Missile Subs. Increased the submarine detection range of Gunboat from 3 to 4. Fixed Spies having an enemy color health bar when disguised as a friendly unit (occurred using the Team Health Colors setting). + Chrono Tanks can now be teleported in groups by holding SHIFT or ALT and issuing a move order. Tiberian Dawn: Chinook rotors now counter-rotate. Commando can now plant C4 on bridges. @@ -129,6 +130,8 @@ NEW: Muzzleflash definitions have moved from WithMuzzleFlash to each Armament. Only one WithMuzzleFlash is now required per actor. Added an AttackGarrisoned trait that allows passengers to fire through a set of defined ports. Maps can define initial cargo for actors by adding "Cargo: actora, actorb, ..." to the actor reference. + Modified Teleport activity to use the best/closest open cell to the target destination for teleports (for ChronoshiftPower this only applies on the return trip). + Renamed ChronoshiftDeploy trait to PortableChrono. Server: Message of the day is now shared between all mods and a default motd.txt gets created in the user directory. Asset Browser: diff --git a/OpenRA.Game/WorldUtils.cs b/OpenRA.Game/WorldUtils.cs index 7dbdaa9503..423e97da10 100755 --- a/OpenRA.Game/WorldUtils.cs +++ b/OpenRA.Game/WorldUtils.cs @@ -85,7 +85,7 @@ namespace OpenRA return ts; } - const int MaxRange = 50; + public const int MaxRange = 50; static List[] TilesByDistance = InitTilesByDistance(MaxRange); public static string GetTerrainType(this World world, CPos cell) diff --git a/OpenRA.Mods.RA/Activities/Teleport.cs b/OpenRA.Mods.RA/Activities/Teleport.cs index 6ac8aae624..05197c3ace 100755 --- a/OpenRA.Mods.RA/Activities/Teleport.cs +++ b/OpenRA.Mods.RA/Activities/Teleport.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2011 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2014 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. For more information, @@ -8,6 +8,9 @@ */ #endregion +using System; +using System.Collections.Generic; +using System.Linq; using OpenRA.Traits; using OpenRA.Mods.RA.Render; @@ -15,21 +18,40 @@ namespace OpenRA.Mods.RA.Activities { public class Teleport : Activity { - CPos destination; - bool killCargo; Actor chronosphere; + CPos destination; + int? maximumDistance; + bool killCargo; + bool screenFlash; string sound; - public Teleport(Actor chronosphere, CPos destination, bool killCargo, string sound) + const int maxCellSearchRange = WorldUtils.MaxRange; + + public Teleport(Actor chronosphere, CPos destination, int? maximumDistance, bool killCargo, bool screenFlash, string sound) { + if (maximumDistance > maxCellSearchRange) + throw new InvalidOperationException("Teleport cannot be used with a maximum teleport distance greater than {0}.".F(maxCellSearchRange)); + this.chronosphere = chronosphere; this.destination = destination; + this.maximumDistance = maximumDistance; this.killCargo = killCargo; + this.screenFlash = screenFlash; this.sound = sound; } public override Activity Tick(Actor self) { + var pc = self.TraitOrDefault(); + if (pc != null && !pc.CanTeleport) + return NextActivity; + + var bestCell = ChooseBestDestinationCell(self, destination); + if (bestCell == null) + return NextActivity; + + destination = bestCell.Value; + Sound.Play(sound, self.CenterPosition); Sound.Play(sound, destination.CenterPosition); @@ -51,15 +73,48 @@ namespace OpenRA.Mods.RA.Activities } } + // Consume teleport charges if this wasn't triggered via chronosphere + if (chronosphere == null && pc != null) + pc.ResetChargeTime(); + // Trigger screen desaturate effect - foreach (var a in self.World.ActorsWithTrait()) - a.Trait.Enable(); + if (screenFlash) + foreach (var a in self.World.ActorsWithTrait()) + a.Trait.Enable(); if (chronosphere != null && !chronosphere.Destroyed && chronosphere.HasTrait()) chronosphere.Trait().PlayCustomAnim(chronosphere, "active"); return NextActivity; } + + CPos? ChooseBestDestinationCell(Actor self, CPos destination) + { + var restrictTo = maximumDistance == null ? null : self.World.FindTilesInCircle(self.Location, maximumDistance.Value); + + if (maximumDistance != null) + destination = restrictTo.OrderBy(x => (x - destination).Length).First(); + + var pos = self.Trait(); + if (pos.CanEnterCell(destination) && self.Owner.Shroud.IsExplored(destination)) + return destination; + + var searched = new List(); + for (int r = 1; r <= maxCellSearchRange || (maximumDistance != null && r <= maximumDistance); r++) + { + foreach (var tile in self.World.FindTilesInCircle(destination, r).Except(searched)) + { + if (self.Owner.Shroud.IsExplored(tile) + && (restrictTo == null || (restrictTo != null && restrictTo.Contains(tile))) + && pos.CanEnterCell(tile)) + return tile; + + searched.Add(tile); + } + } + + return null; + } } public class SimpleTeleport : Activity diff --git a/OpenRA.Mods.RA/ChronoshiftDeploy.cs b/OpenRA.Mods.RA/ChronoshiftDeploy.cs deleted file mode 100644 index d6ab9cee87..0000000000 --- a/OpenRA.Mods.RA/ChronoshiftDeploy.cs +++ /dev/null @@ -1,163 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2011 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. For more information, - * see COPYING. - */ -#endregion - -using System.Drawing; -using System.Collections.Generic; -using OpenRA.Mods.RA.Activities; -using OpenRA.Mods.RA.Orders; -using OpenRA.Traits; -using OpenRA.Graphics; - -namespace OpenRA.Mods.RA -{ - class ChronoshiftDeployInfo : ITraitInfo - { - public readonly int ChargeTime = 30; // seconds - public readonly int JumpDistance = 10; - public readonly string ChronoshiftSound = "chrotnk1.aud"; - - public object Create(ActorInitializer init) { return new ChronoshiftDeploy(init.self, this); } - } - - class ChronoshiftDeploy : IIssueOrder, IResolveOrder, ITick, IPips, IOrderVoice, ISync - { - [Sync] int chargeTick = 0; - public readonly ChronoshiftDeployInfo Info; - readonly Actor self; - - public ChronoshiftDeploy(Actor self, ChronoshiftDeployInfo info) - { - this.self = self; - this.Info = info; - } - - public void Tick(Actor self) - { - if (chargeTick > 0) - chargeTick--; - } - - public IEnumerable Orders - { - get { yield return new DeployOrderTargeter("ChronoshiftJump", 5, () => chargeTick <= 0); } - } - - public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) - { - if (order.OrderID == "ChronoshiftJump" && chargeTick <= 0) - self.World.OrderGenerator = new ChronoTankOrderGenerator(self); - - return new Order("ChronoshiftJump", self, false); // Hack until we can return null - } - - public void ResolveOrder(Actor self, Order order) - { - if (order.OrderString == "ChronoshiftJump") - { - if (CanJumpTo(order.TargetLocation, true)) - { - self.CancelActivity(); - self.QueueActivity(new Teleport(null, order.TargetLocation, true, Info.ChronoshiftSound)); - chargeTick = 25 * Info.ChargeTime; - } - } - } - - public string VoicePhraseForOrder(Actor self, Order order) - { - return (order.OrderString == "ChronoshiftDeploy" && chargeTick <= 0) ? "Move" : null; - } - - // Display 2 pips indicating the current charge status - public IEnumerable GetPips(Actor self) - { - const int numPips = 2; - for (int i = 0; i < numPips; i++) - { - if ((1 - chargeTick * 1.0f / (25 * Info.ChargeTime)) * numPips < i + 1) - { - yield return PipType.Transparent; - continue; - } - - yield return PipType.Blue; - } - } - - public bool CanJumpTo(CPos xy, bool ignoreVis) - { - var movement = self.TraitOrDefault(); - - if (chargeTick <= 0 // Can jump - && (self.Location - xy).Length <= Info.JumpDistance // Within jump range - && movement.CanEnterCell(xy) // Can enter cell - && (ignoreVis || self.Owner.Shroud.IsExplored(xy))) // Not in shroud - return true; - else - return false; - } - } - - class ChronoTankOrderGenerator : IOrderGenerator - { - readonly Actor self; - - public ChronoTankOrderGenerator(Actor self) { this.self = self; } - - public IEnumerable Order(World world, CPos xy, MouseInput mi) - { - if (mi.Button == Game.mouseButtonPreference.Cancel) - { - world.CancelInputMode(); - yield break; - } - - var queued = mi.Modifiers.HasModifier(Modifiers.Shift); - - var cinfo = self.Trait(); - if (cinfo.CanJumpTo(xy, false)) - { - self.World.CancelInputMode(); - yield return new Order("ChronoshiftJump", self, queued) { TargetLocation = xy }; - } - } - - public string GetCursor(World world, CPos xy, MouseInput mi) - { - if (self.IsInWorld && self.Trait().CanJumpTo(xy,false)) - return "chrono-target"; - else - return "move-blocked"; - } - - public void Tick(World world) - { - if (!self.IsInWorld || self.IsDead()) - world.CancelInputMode(); - } - - public IEnumerable Render(WorldRenderer wr, World world) { yield break; } - public void RenderAfterWorld(WorldRenderer wr, World world) - { - if (!self.IsInWorld) - return; - - if (self.Owner != self.World.LocalPlayer) - return; - - wr.DrawRangeCircleWithContrast( - self.CenterPosition, - WRange.FromCells(self.Trait().Info.JumpDistance), - Color.FromArgb(128, Color.DeepSkyBlue), - Color.FromArgb(96, Color.Black) - ); - } - } -} diff --git a/OpenRA.Mods.RA/Chronoshiftable.cs b/OpenRA.Mods.RA/Chronoshiftable.cs index da7311defe..7de0496a14 100755 --- a/OpenRA.Mods.RA/Chronoshiftable.cs +++ b/OpenRA.Mods.RA/Chronoshiftable.cs @@ -54,9 +54,7 @@ namespace OpenRA.Mods.RA if (--ReturnTicks == 0) { self.CancelActivity(); - - // TODO: need a new Teleport method that will move to the closest available cell - self.QueueActivity(new Teleport(chronosphere, Origin, killCargo, info.ChronoshiftSound)); + self.QueueActivity(new Teleport(chronosphere, Origin, null, killCargo, true, info.ChronoshiftSound)); } } @@ -90,7 +88,7 @@ namespace OpenRA.Mods.RA // Set up the teleport self.CancelActivity(); - self.QueueActivity(new Teleport(chronosphere, targetLocation, killCargo, info.ChronoshiftSound)); + self.QueueActivity(new Teleport(chronosphere, targetLocation, null, killCargo, true, info.ChronoshiftSound)); return true; } diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 869c7ea7e7..1599515eab 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -190,7 +190,6 @@ - @@ -293,6 +292,7 @@ + @@ -453,7 +453,6 @@ - diff --git a/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs b/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs deleted file mode 100644 index 16315395f0..0000000000 --- a/OpenRA.Mods.RA/Orders/SetChronoTankDestination.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2011 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. For more information, - * see COPYING. - */ -#endregion - -using System.Collections.Generic; -using System.Drawing; -using OpenRA.Graphics; -using OpenRA.Traits; - -namespace OpenRA.Mods.RA.Orders -{ - class SetChronoTankDestination : IOrderGenerator - { - public readonly Actor self; - - public SetChronoTankDestination(Actor self) - { - this.self = self; - } - - public IEnumerable Order(World world, CPos xy, MouseInput mi) - { - if (mi.Button == MouseButton.Left) - { - world.CancelInputMode(); - yield break; - } - - var queued = mi.Modifiers.HasModifier(Modifiers.Shift); - if (world.LocalPlayer.Shroud.IsExplored(xy)) - yield return new Order("ChronoshiftSelf", self, queued) { TargetLocation = xy }; - } - - public void Tick( World world ) { } - public IEnumerable Render(WorldRenderer wr, World world) { yield break; } - public void RenderAfterWorld( WorldRenderer wr, World world ) - { - wr.DrawSelectionBox(self, Color.White); - } - - public string GetCursor(World world, CPos xy, MouseInput mi) - { - if (!world.LocalPlayer.Shroud.IsExplored(xy)) - return "move-blocked"; - - var movement = self.TraitOrDefault(); - return (movement.CanEnterCell(xy)) ? "chrono-target" : "move-blocked"; - } - } -} diff --git a/OpenRA.Mods.RA/PortableChrono.cs b/OpenRA.Mods.RA/PortableChrono.cs new file mode 100644 index 0000000000..3c62237c08 --- /dev/null +++ b/OpenRA.Mods.RA/PortableChrono.cs @@ -0,0 +1,201 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 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. For more information, + * see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Drawing; +using OpenRA.Mods.RA.Activities; +using OpenRA.Mods.RA.Orders; +using OpenRA.FileFormats; +using OpenRA.Traits; +using OpenRA.Graphics; + +namespace OpenRA.Mods.RA +{ + class PortableChronoInfo : ITraitInfo + { + [Desc("Cooldown in seconds until the unit can teleport.")] + public readonly int ChargeTime = 30; + [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 = 10; + [Desc("Sound to play when teleporting.")] + public readonly string ChronoshiftSound = "chrotnk1.aud"; + + public object Create(ActorInitializer init) { return new PortableChrono(this); } + } + + class PortableChrono : IIssueOrder, IResolveOrder, ITick, IPips, IOrderVoice, ISync + { + [Sync] int chargeTick = 0; + public readonly PortableChronoInfo Info; + + public PortableChrono(PortableChronoInfo info) + { + this.Info = info; + } + + public void Tick(Actor self) + { + if (chargeTick > 0) + chargeTick--; + } + + public IEnumerable Orders + { + get + { + yield return new PortableChronoOrderTargeter(); + yield return new DeployOrderTargeter("PortableChronoDeploy", 5, () => CanTeleport); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) + { + if (order.OrderID == "PortableChronoDeploy" && CanTeleport) + self.World.OrderGenerator = new PortableChronoOrderGenerator(self); + + if (order.OrderID == "PortableChronoTeleport") + return new Order(order.OrderID, self, queued) { TargetLocation = target.CenterPosition.ToCPos() }; + + return new Order(order.OrderID, self, queued) { TargetActor = target.Actor }; + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString == "PortableChronoTeleport" && CanTeleport) + { + var maxDistance = Info.HasDistanceLimit ? Info.MaxDistance : (int?)null; + self.CancelActivity(); + self.QueueActivity(new Teleport(null, order.TargetLocation, maxDistance, true, false, Info.ChronoshiftSound)); + } + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + return order.OrderString == "PortableChronoTeleport" && CanTeleport ? "Move" : null; + } + + public void ResetChargeTime() + { + chargeTick = 25 * Info.ChargeTime; + } + + public bool CanTeleport + { + get { return chargeTick <= 0; } + } + + // Display 2 pips indicating the current charge status + public IEnumerable GetPips(Actor self) + { + const int numPips = 2; + for (int i = 0; i < numPips; i++) + { + if ((1 - chargeTick * 1.0f / (25 * Info.ChargeTime)) * numPips < i + 1) + { + yield return PipType.Transparent; + continue; + } + + yield return PipType.Blue; + } + } + } + + class PortableChronoOrderTargeter : IOrderTargeter + { + public string OrderID { get { return "PortableChronoTeleport"; } } + public int OrderPriority { get { return 5; } } + public bool IsQueued { get; protected set; } + + public bool CanTarget(Actor self, Target target, List othersAtTarget, TargetModifiers modifiers, ref string cursor) + { + // TODO: When target modifiers are configurable this needs to be revisited + if (modifiers.HasModifier(TargetModifiers.ForceMove) || modifiers.HasModifier(TargetModifiers.ForceQueue)) + { + var xy = target.CenterPosition.ToCPos(); + + IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); + + if (self.IsInWorld && self.Owner.Shroud.IsExplored(xy)) + { + cursor = "chrono-target"; + return true; + } + return false; + } + + return false; + } + } + + class PortableChronoOrderGenerator : IOrderGenerator + { + readonly Actor self; + + public PortableChronoOrderGenerator(Actor self) + { + this.self = self; + } + + public IEnumerable Order(World world, CPos xy, MouseInput mi) + { + if (mi.Button == Game.mouseButtonPreference.Cancel) + { + world.CancelInputMode(); + yield break; + } + + if (self.IsInWorld && self.CenterPosition.ToCPos() != xy + && self.Trait().CanTeleport && self.Owner.Shroud.IsExplored(xy)) + { + world.CancelInputMode(); + yield return new Order("PortableChronoTeleport", self, mi.Modifiers.HasModifier(Modifiers.Shift)) { TargetLocation = xy }; + } + } + + public void Tick(World world) + { + if (!self.IsInWorld || self.IsDead()) + world.CancelInputMode(); + } + + public IEnumerable Render(WorldRenderer wr, World world) + { + yield break; + } + + public void RenderAfterWorld(WorldRenderer wr, World world) + { + if (!self.IsInWorld || self.Owner != self.World.LocalPlayer) + return; + + if (!self.Trait().Info.HasDistanceLimit) + return; + + wr.DrawRangeCircleWithContrast( + self.CenterPosition, + WRange.FromCells(self.Trait().Info.MaxDistance), + Color.FromArgb(128, Color.LawnGreen), + Color.FromArgb(96, Color.Black) + ); + } + + public string GetCursor(World world, CPos xy, MouseInput mi) + { + if (self.IsInWorld && self.CenterPosition.ToCPos() != xy + && self.Trait().CanTeleport && self.Owner.Shroud.IsExplored(xy)) + return "chrono-target"; + else + return "move-blocked"; + } + } +} diff --git a/OpenRA.Utility/UpgradeRules.cs b/OpenRA.Utility/UpgradeRules.cs index aca505c694..7105d21b51 100644 --- a/OpenRA.Utility/UpgradeRules.cs +++ b/OpenRA.Utility/UpgradeRules.cs @@ -222,6 +222,16 @@ namespace OpenRA.Utility } } + // ChronoshiftDeploy was replaced with PortableChrono + if (engineVersion < 20140321) + { + if (depth == 1 && node.Key == "ChronoshiftDeploy") + node.Key = "PortableChrono"; + + if (depth == 2 && parentKey == "PortableChrono" && node.Key == "JumpDistance") + node.Key = "MaxDistance"; + } + UpgradeActorRules(engineVersion, ref node.Value.Nodes, node, depth + 1); } } diff --git a/mods/ra/rules/vehicles.yaml b/mods/ra/rules/vehicles.yaml index 6577fad977..17057bccc2 100644 --- a/mods/ra/rules/vehicles.yaml +++ b/mods/ra/rules/vehicles.yaml @@ -693,7 +693,7 @@ CTNK: LocalOffset: 0,171,0 LocalYaw: -100 AttackFrontal: - ChronoshiftDeploy: + PortableChrono: QTNK: Inherits: ^Tank