diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index ce4c980fff..73c779a779 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -802,6 +802,8 @@ + + diff --git a/OpenRA.Mods.Common/Traits/EntersTunnels.cs b/OpenRA.Mods.Common/Traits/EntersTunnels.cs new file mode 100644 index 0000000000..36480467b3 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/EntersTunnels.cs @@ -0,0 +1,132 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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.Mods.Common.Orders; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("This actor can interact with TunnelEntrances to move through TerrainTunnels.")] + public class EntersTunnelsInfo : ITraitInfo, Requires + { + public readonly string EnterCursor = "enter"; + public readonly string EnterBlockedCursor = "enter-blocked"; + + [VoiceReference] public readonly string Voice = "Action"; + + public object Create(ActorInitializer init) { return new EntersTunnels(init.Self, this); } + } + + public class EntersTunnels : IIssueOrder, IResolveOrder, IOrderVoice + { + readonly EntersTunnelsInfo info; + readonly IMove move; + + public EntersTunnels(Actor self, EntersTunnelsInfo info) + { + this.info = info; + move = self.Trait(); + } + + public IEnumerable Orders + { + get + { + yield return new EnterTunnelOrderTargeter(info); + } + } + + public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) + { + if (order.OrderID != "EnterTunnel") + return null; + + if (target.Type == TargetType.FrozenActor) + return new Order(order.OrderID, self, queued) { ExtraData = target.FrozenActor.ID, SuppressVisualFeedback = true }; + + return new Order(order.OrderID, self, queued) { TargetActor = target.Actor, SuppressVisualFeedback = true }; + } + + public string VoicePhraseForOrder(Actor self, Order order) + { + return order.OrderString == "EnterTunnel" ? info.Voice : null; + } + + public void ResolveOrder(Actor self, Order order) + { + if (order.OrderString != "EnterTunnel") + return; + + var target = self.ResolveFrozenActorOrder(order, Color.Red); + if (target.Type != TargetType.Actor) + return; + + var tunnel = target.Actor.TraitOrDefault(); + if (!tunnel.Exit.HasValue) + return; + + if (!order.Queued) + self.CancelActivity(); + + self.SetTargetLine(Target.FromCell(self.World, tunnel.Exit.Value), Color.Green); + self.QueueActivity(move.MoveTo(tunnel.Entrance, tunnel.NearEnough)); + self.QueueActivity(move.MoveTo(tunnel.Exit.Value, tunnel.NearEnough)); + } + + class EnterTunnelOrderTargeter : UnitOrderTargeter + { + readonly EntersTunnelsInfo info; + + public EnterTunnelOrderTargeter(EntersTunnelsInfo info) + : base("EnterTunnel", 6, info.EnterCursor, true, true) + { + this.info = info; + } + + public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) + { + if (target.IsDead) + return false; + + var tunnel = target.TraitOrDefault(); + if (tunnel == null) + return false; + + // HACK: The engine does not support HiddenUnderFog combined with buildings that use the "_" footprint + // We therefore have to use AlwaysVisible and then force-disable interacting with the entrance under shroud + var buildingInfo = target.Info.TraitInfoOrDefault(); + if (buildingInfo != null) + { + var footprint = FootprintUtils.PathableTiles(target.Info.Name, buildingInfo, target.Location); + if (footprint.All(c => self.World.ShroudObscures(c))) + return false; + } + + if (!tunnel.Exit.HasValue) + { + cursor = info.EnterBlockedCursor; + return false; + } + + cursor = info.EnterCursor; + return true; + } + + public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) + { + return CanTargetActor(self, target.Actor, modifiers, ref cursor); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/TunnelEntrance.cs b/OpenRA.Mods.Common/Traits/TunnelEntrance.cs new file mode 100644 index 0000000000..8e4afcef64 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/TunnelEntrance.cs @@ -0,0 +1,74 @@ +#region Copyright & License Information +/* + * Copyright 2007-2017 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.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Provides a target for players to issue orders for units to move through a TerrainTunnel.", + "The host actor should be placed so that the Sensor position overlaps one of the TerrainTunnel portal cells.")] + public class TunnelEntranceInfo : ITraitInfo + { + [FieldLoader.Require] + [Desc("Offset to use as a staging point for actors entering or exiting the tunnel.", + "Should be at least Margin cells away from the actual entrance.")] + public readonly CVec RallyPoint = CVec.Zero; + + [Desc("Cell radius to use as a staging area around the RallyPoint.")] + public readonly int Margin = 2; + + [Desc("Offset to check for the corresponding TerrainTunnel portal cell(s).")] + public readonly CVec Sensor = CVec.Zero; + + public object Create(ActorInitializer init) { return new TunnelEntrance(init.Self, this); } + } + + public class TunnelEntrance : INotifyCreated + { + readonly TunnelEntranceInfo info; + + public readonly CPos Entrance; + public CPos? Exit { get; private set; } + public int NearEnough { get { return info.Margin; } } + + public TunnelEntrance(Actor self, TunnelEntranceInfo info) + { + this.info = info; + + Entrance = self.Location + info.RallyPoint; + } + + void INotifyCreated.Created(Actor self) + { + // Find the map tunnel associated with this entrance + var sensor = self.Location + info.Sensor; + var tunnel = self.World.WorldActor.Info.TraitInfos() + .FirstOrDefault(tti => tti.PortalCells().Contains(sensor)); + + if (tunnel != null) + { + // Find the matching entrance at the other end of the tunnel + // Run at the end of the tick to make sure that all the entrances exist in the world + self.World.AddFrameEndTask(w => + { + var portalCells = tunnel.PortalCells().ToList(); + var other = self.World.ActorsWithTrait() + .FirstOrDefault(x => x.Actor != self && portalCells.Contains(x.Actor.Location + x.Trait.info.Sensor)); + + if (other.Trait != null) + Exit = other.Trait.Entrance; + }); + } + } + } +} diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index a9c635d3ba..ffafb54be1 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -381,6 +381,8 @@ ReferencePoint: Bottom, Right RequiresCondition: hospitalheal RevealOnFire: + EntersTunnels: + Voice: Move ^RegularInfantryDeath: WithDeathAnimation@normal: @@ -565,6 +567,8 @@ Carryable: RequiresCondition: !inside-tunnel RevealOnFire: + EntersTunnels: + Voice: Move ^Tank: Inherits: ^Vehicle @@ -710,6 +714,8 @@ Guardable: WithFacingSpriteBody: RevealOnFire: + EntersTunnels: + Voice: Move ^BlossomTree: Inherits@1: ^SpriteActor @@ -883,6 +889,8 @@ CustomBounds: 144, 144 Targetable: AlwaysVisible: + TunnelEntrance: + Dimensions: 3, 3 ^Gate: Inherits: ^Building diff --git a/mods/ts/rules/misc.yaml b/mods/ts/rules/misc.yaml index 45d14be609..3d72f4e558 100644 --- a/mods/ts/rules/misc.yaml +++ b/mods/ts/rules/misc.yaml @@ -213,12 +213,24 @@ TRACKS16: TUNTOP01: Inherits: ^Tunnel + TunnelEntrance: + RallyPoint: -3, 1 + Sensor: 0, 1 TUNTOP02: Inherits: ^Tunnel + TunnelEntrance: + RallyPoint: 1, -3 + Sensor: 1, 0 TUNTOP03: Inherits: ^Tunnel + TunnelEntrance: + RallyPoint: 3, 1 + Sensor: 0, 1 TUNTOP04: Inherits: ^Tunnel + TunnelEntrance: + RallyPoint: 1, 3 + Sensor: 1, 0