diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 733ba6259f..9928f29a32 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -803,6 +803,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/maps/arivruns/map.yaml b/mods/ts/maps/arivruns/map.yaml
index 9f9afa2fe8..aead5c973a 100644
--- a/mods/ts/maps/arivruns/map.yaml
+++ b/mods/ts/maps/arivruns/map.yaml
@@ -1552,16 +1552,16 @@ Actors:
Owner: Neutral
Actor620: tuntop04
Owner: Neutral
- Location: 145,5
+ Location: 146,5
Actor621: tuntop04
Owner: Neutral
- Location: 116,9
+ Location: 117,9
Actor622: tuntop02
Owner: Neutral
- Location: 144,-19
+ Location: 146,-19
Actor623: tuntop02
Owner: Neutral
- Location: 115,-15
+ Location: 117,-15
Actor494: bridge1
Owner: Neutral
Location: 232,0
diff --git a/mods/ts/maps/springs/map.yaml b/mods/ts/maps/springs/map.yaml
index 5fb470c3ca..73d51e0f05 100644
--- a/mods/ts/maps/springs/map.yaml
+++ b/mods/ts/maps/springs/map.yaml
@@ -806,49 +806,49 @@ Actors:
Owner: Neutral
Actor219: tuntop03
Owner: Neutral
- Location: 68,-5
+ Location: 68,-4
Actor220: tuntop04
Owner: Neutral
- Location: 73,-12
+ Location: 74,-12
Actor221: tuntop04
Owner: Neutral
- Location: 107,-7
+ Location: 108,-7
Actor222: tuntop03
Owner: Neutral
- Location: 109,4
+ Location: 109,5
Actor224: tuntop04
Owner: Neutral
- Location: 87,28
+ Location: 88,28
Actor225: tuntop03
Owner: Neutral
- Location: 73,34
+ Location: 73,35
Actor227: tuntop01
Owner: Neutral
- Location: 60,34
+ Location: 60,35
Actor228: tuntop04
Owner: Neutral
- Location: 42,13
+ Location: 43,13
Actor233: tuntop03
Owner: Neutral
- Location: 93,-37
+ Location: 93,-36
Actor237: tuntop02
Owner: Neutral
- Location: 107,-16
+ Location: 108,-16
Actor238: tuntop01
Owner: Neutral
- Location: 77,-37
+ Location: 77,-36
Actor239: tuntop01
Owner: Neutral
- Location: 95,4
+ Location: 95,5
Actor240: tuntop02
Owner: Neutral
Location: 73,-28
Actor234: tuntop02
Owner: Neutral
- Location: 42,-2
+ Location: 43,-2
Actor236: tuntop01
Owner: Neutral
- Location: 51,-5
+ Location: 51,-4
Actor235: tuntop02
Owner: Neutral
Location: 88,13
diff --git a/mods/ts/maps/sunstroke/map.png b/mods/ts/maps/sunstroke/map.png
index dbec7ee702..0bacd1cd87 100644
Binary files a/mods/ts/maps/sunstroke/map.png and b/mods/ts/maps/sunstroke/map.png differ
diff --git a/mods/ts/maps/sunstroke/map.yaml b/mods/ts/maps/sunstroke/map.yaml
index 0ca6d2b19f..ad61a2ff5f 100644
--- a/mods/ts/maps/sunstroke/map.yaml
+++ b/mods/ts/maps/sunstroke/map.yaml
@@ -2377,23 +2377,23 @@ Actors:
Actor679: srock05
Location: 144,-8
Owner: Neutral
-# Actor680: lobrdg_r_nw
-# Location: 152,-16
-# Owner: Neutral
+ Actor680: lobrdg_r_nw
+ Location: 152,-17
+ Owner: Neutral
Actor681: bridge2
Location: 140,-3
Owner: Neutral
-# Actor682: lobrdg_b
-# Location: 153,-16
-# Owner: Neutral
-# Health: 50
+ Actor682: lobrdg_b
+ Location: 153,-17
+ Owner: Neutral
+ Health: 50
Actor683: srock04
Location: 145,-7
Owner: Neutral
-# Actor684: lobrdg_b
-# Location: 154,-16
-# Owner: Neutral
-# Health: 25
+ Actor684: lobrdg_b
+ Location: 154,-17
+ Owner: Neutral
+ Health: 25
Actor685: crat01
Location: 115,24
Owner: Neutral
@@ -2487,10 +2487,10 @@ Actors:
Actor715: crat03
Location: 105,50
Owner: Neutral
-# Actor716: lobrdg_r_se
-# Location: 171,-16
-# Owner: Neutral
-# Health: 50
+ Actor716: lobrdg_r_se
+ Location: 171,-17
+ Owner: Neutral
+ Health: 50
Actor717: trock05
Location: 189,-34
Owner: Neutral
@@ -2564,6 +2564,15 @@ Actors:
Owner: Creeps
Location: 58,23
Facing: 92
+ Actor741: tuntop04
+ Owner: Neutral
+ Location: 183,-43
+ Actor742: lobrdg_b_d
+ Owner: Neutral
+ Location: 155,-17
+ Actor743: lobrdg_b_d
+ Owner: Neutral
+ Location: 170,-17
Rules: rules.yaml
diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml
index 02422a42fb..e5db3a514e 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
@@ -870,9 +876,21 @@
TerrainType: Rail
^Tunnel:
- Inherits: ^TerrainOverlay
+ RenderSprites:
+ Palette: terraindecoration
+ WithSpriteBody:
+ BodyOrientation:
+ UseClassicPerspectiveFudge: False
+ QuantizedFacings: 1
+ Building:
+ Dimensions: 3, 3
+ Footprint: ___ ___ ___
CustomSelectionSize:
- CustomBounds: 220,220
+ 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
diff --git a/mods/ts/sequences/misc.yaml b/mods/ts/sequences/misc.yaml
index 555bdaca70..4ad1901fc9 100644
--- a/mods/ts/sequences/misc.yaml
+++ b/mods/ts/sequences/misc.yaml
@@ -614,24 +614,28 @@ tracks16:
^tuntop:
idle:
- Offset: 0, -13, 42
ZRamp: 1
UseTilesetExtension: true
- # Disabled to avoid glitches until shadow rendering is fixed
- # Appears to be unused in the original game too
- # ShadowStart: 1
tuntop01:
Inherits: ^tuntop
+ idle:
+ Offset: 24, -49, 36
tuntop02:
Inherits: ^tuntop
+ idle:
+ Offset: -24, -49, 36
tuntop03:
Inherits: ^tuntop
+ idle:
+ Offset: 24, -49, 24
tuntop04:
Inherits: ^tuntop
+ idle:
+ Offset: -24, -49, 24
dig:
idle: