diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 4978ff8c3c..3dbfd11e0b 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -797,6 +797,7 @@
+
diff --git a/OpenRA.Mods.Common/Traits/Mobile.cs b/OpenRA.Mods.Common/Traits/Mobile.cs
index 3e719cf2e9..4692bd7067 100644
--- a/OpenRA.Mods.Common/Traits/Mobile.cs
+++ b/OpenRA.Mods.Common/Traits/Mobile.cs
@@ -43,6 +43,7 @@ namespace OpenRA.Mods.Common.Traits
{
public const byte Tunnel = 1;
public const byte Subterranean = 2;
+ public const byte Jumpjet = 3;
}
[Desc("Unit is able to move.")]
@@ -115,6 +116,22 @@ namespace OpenRA.Mods.Common.Traits
public readonly string SubterraneanTransitionSound = null;
+ [Desc("Can this unit fly over obsticals?")]
+ public readonly bool Jumpjet = false;
+
+ [GrantedConditionReference]
+ [Desc("The condition to grant to self while flying.")]
+ public readonly string JumpjetCondition = null;
+
+ [Desc("Pathfinding cost for taking off or landing.")]
+ public readonly int JumpjetTransitionCost = 0;
+
+ [Desc("The terrain types that this actor can transition on. Leave empty to allow any.")]
+ public readonly HashSet JumpjetTransitionTerrainTypes = new HashSet();
+
+ [Desc("Can this actor transition on slopes?")]
+ public readonly bool JumpjetTransitionOnRamps = true;
+
public override object Create(ActorInitializer init) { return new Mobile(init, this); }
static object LoadSpeeds(MiniYaml y)
@@ -378,6 +395,7 @@ namespace OpenRA.Mods.Common.Traits
public SubCell FromSubCell, ToSubCell;
int tunnelToken = ConditionManager.InvalidConditionToken;
int subterraneanToken = ConditionManager.InvalidConditionToken;
+ int jumpjetToken = ConditionManager.InvalidConditionToken;
ConditionManager conditionManager;
[Sync] public int Facing
@@ -424,6 +442,12 @@ namespace OpenRA.Mods.Common.Traits
if (!string.IsNullOrEmpty(Info.SubterraneanTransitionSound))
Game.Sound.Play(SoundType.World, Info.SubterraneanTransitionSound);
}
+
+ // Grant the jumpjet condition as soon as the actor starts leaving the ground layer
+ // The condition is revoked from FinishedMoving
+ if (toCell.Layer == CustomMovementLayerType.Jumpjet && conditionManager != null &&
+ !string.IsNullOrEmpty(Info.JumpjetCondition) && jumpjetToken == ConditionManager.InvalidConditionToken)
+ jumpjetToken = conditionManager.GrantCondition(self, Info.JumpjetCondition);
}
public Mobile(ActorInitializer init, MobileInfo info)
@@ -699,6 +723,11 @@ namespace OpenRA.Mods.Common.Traits
public void FinishedMoving(Actor self)
{
+ // Need to check both fromCell and toCell because FinishedMoving is called multiple times during the move
+ // and that condition guarantees that this only runs when the unit has finished landing.
+ if (fromCell.Layer != CustomMovementLayerType.Jumpjet && toCell.Layer != CustomMovementLayerType.Jumpjet && jumpjetToken != ConditionManager.InvalidConditionToken)
+ jumpjetToken = conditionManager.RevokeCondition(self, jumpjetToken);
+
// Only make actor crush if it is on the ground
if (!self.IsAtGroundLevel())
return;
diff --git a/OpenRA.Mods.Common/Traits/World/DomainIndex.cs b/OpenRA.Mods.Common/Traits/World/DomainIndex.cs
index 9646e555d9..edac9623b2 100644
--- a/OpenRA.Mods.Common/Traits/World/DomainIndex.cs
+++ b/OpenRA.Mods.Common/Traits/World/DomainIndex.cs
@@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.Traits
return true;
// HACK: Workaround until we can generalize movement classes
- if (mi.Subterranean)
+ if (mi.Subterranean || mi.Jumpjet)
return true;
return domainIndexes[movementClass].IsPassable(p1, p2);
diff --git a/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs b/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs
new file mode 100644
index 0000000000..ccfc0e5268
--- /dev/null
+++ b/OpenRA.Mods.Common/Traits/World/JumpjetActorLayer.cs
@@ -0,0 +1,103 @@
+#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.Linq;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Traits
+{
+ public class JumpjetActorLayerInfo : ITraitInfo
+ {
+ [Desc("Terrain type of the airborne layer.")]
+ public readonly string TerrainType = "Jumpjet";
+
+ [Desc("Height offset relative to the smoothed terrain for movement.")]
+ public readonly WDist HeightOffset = new WDist(2304);
+
+ [Desc("Cell radius for smoothing adjacent cell heights.")]
+ public readonly int SmoothingRadius = 2;
+
+ public object Create(ActorInitializer init) { return new JumpjetActorLayer(init.Self, this); }
+ }
+
+ public class JumpjetActorLayer : ICustomMovementLayer
+ {
+ readonly Map map;
+
+ readonly byte terrainIndex;
+ readonly CellLayer height;
+
+ public JumpjetActorLayer(Actor self, JumpjetActorLayerInfo info)
+ {
+ map = self.World.Map;
+ terrainIndex = self.World.Map.Rules.TileSet.GetTerrainIndex(info.TerrainType);
+ height = new CellLayer(map);
+ foreach (var c in map.AllCells)
+ {
+ var neighbourCount = 0;
+ var neighbourHeight = 0;
+ for (var dy = -info.SmoothingRadius; dy <= info.SmoothingRadius; dy++)
+ {
+ for (var dx = -info.SmoothingRadius; dx <= info.SmoothingRadius; dx++)
+ {
+ var neighbour = c + new CVec(dx, dy);
+ if (!map.AllCells.Contains(neighbour))
+ continue;
+
+ neighbourCount++;
+ neighbourHeight += map.Height[neighbour];
+ }
+ }
+
+ height[c] = info.HeightOffset.Length + neighbourHeight * 512 / neighbourCount;
+ }
+ }
+
+ bool ICustomMovementLayer.EnabledForActor(ActorInfo a, MobileInfo mi) { return mi.Jumpjet; }
+ byte ICustomMovementLayer.Index { get { return CustomMovementLayerType.Jumpjet; } }
+ bool ICustomMovementLayer.InteractsWithDefaultLayer { get { return true; } }
+
+ WPos ICustomMovementLayer.CenterOfCell(CPos cell)
+ {
+ var pos = map.CenterOfCell(cell);
+ return pos + new WVec(0, 0, height[cell] - pos.Z);
+ }
+
+ bool ValidTransitionCell(CPos cell, MobileInfo mi)
+ {
+ var terrainType = map.GetTerrainInfo(cell).Type;
+ if (!mi.JumpjetTransitionTerrainTypes.Contains(terrainType) && mi.JumpjetTransitionTerrainTypes.Any())
+ return false;
+
+ if (mi.JumpjetTransitionOnRamps)
+ return true;
+
+ var tile = map.Tiles[cell];
+ var ti = map.Rules.TileSet.GetTileInfo(tile);
+ return ti == null || ti.RampType == 0;
+ }
+
+ int ICustomMovementLayer.EntryMovementCost(ActorInfo a, MobileInfo mi, CPos cell)
+ {
+ return ValidTransitionCell(cell, mi) ? mi.JumpjetTransitionCost : int.MaxValue;
+ }
+
+ int ICustomMovementLayer.ExitMovementCost(ActorInfo a, MobileInfo mi, CPos cell)
+ {
+ return ValidTransitionCell(cell, mi) ? mi.JumpjetTransitionCost : int.MaxValue;
+ }
+
+ byte ICustomMovementLayer.GetTerrainIndex(CPos cell)
+ {
+ return terrainIndex;
+ }
+ }
+}
diff --git a/mods/ts/rules/gdi-infantry.yaml b/mods/ts/rules/gdi-infantry.yaml
index 0c3eb63c06..3dd0867c7c 100644
--- a/mods/ts/rules/gdi-infantry.yaml
+++ b/mods/ts/rules/gdi-infantry.yaml
@@ -77,6 +77,11 @@ JUMPJET:
VoiceSet: JumpJet
Mobile:
Speed: 56
+ Jumpjet: True
+ JumpjetTransitionCost: 100
+ JumpjetCondition: airborne
+ TerrainSpeeds:
+ Jumpjet: 110
Health:
HP: 120
Armor:
@@ -91,10 +96,71 @@ JUMPJET:
AttackFrontal:
Voice: Attack
WithInfantryBody:
+ RequiresCondition: !airborne
DefaultAttackSequence: attack
+ WithInfantryBody@flying:
+ RequiresCondition: airborne
+ DefaultAttackSequence: flying-attack
+ StandSequences: flying
+ MoveSequence: flying
-TakeCover:
+ Hovers:
+ RequiresCondition: airborne
ProducibleWithLevel:
Prerequisites: barracks.upgraded
+ Targetable:
+ RequiresCondition: !airborne && !inside-tunnel
+ Targetable@AIRBORNE:
+ TargetTypes: Air
+ RequiresCondition: airborne
+ SpawnActorOnDeath@airborne:
+ Actor: JUMPJET.Husk
+ RequiresCondition: airborne
+ DeathSounds@airborne:
+ RequiresCondition: airborne
+ WithDeathAnimation@normal:
+ RequiresCondition: !airborne
+ WithDeathAnimation@explosion:
+ RequiresCondition: !airborne
+ WithDeathAnimation@energy:
+ RequiresCondition: !airborne
+ WithDeathAnimation:
+ RequiresCondition: !airborne
+ DeathSounds@NORMAL:
+ RequiresCondition: !airborne
+ DeathSounds@EXPLOSION:
+ RequiresCondition: !airborne
+ DeathSounds@BURNED:
+ RequiresCondition: !airborne
+ DeathSounds@ZAPPED:
+ RequiresCondition: !airborne
+ SpawnActorOnDeath:
+ RequiresCondition: !airborne
+ SpawnActorOnDeath@FLAMEGUY:
+ RequiresCondition: !airborne
+
+JUMPJET.Husk:
+ RenderSprites:
+ BodyOrientation:
+ QuantizedFacings: 1
+ Aircraft:
+ HiddenUnderFog:
+ Type: GroundPosition
+ AutoTargetIgnore:
+ ScriptTriggers:
+ Tooltip:
+ Name: Jumpjet Infantry
+ FallsToEarth:
+ Velocity: 86
+ Explosion:
+ Aircraft:
+ Speed: 186
+ CanHover: True
+ RenderSprites:
+ Image: jumpjet
+ AutoSelectionSize:
+ WithSpriteBody:
+ Sequence: die-falling
GHOST:
Inherits: ^Soldier
diff --git a/mods/ts/rules/world.yaml b/mods/ts/rules/world.yaml
index be84eba81e..59aaba54f1 100644
--- a/mods/ts/rules/world.yaml
+++ b/mods/ts/rules/world.yaml
@@ -61,6 +61,7 @@
ExitsDebugOverlayManager:
CliffBackImpassabilityLayer:
SubterraneanActorLayer:
+ JumpjetActorLayer:
World:
Inherits: ^BaseWorld
diff --git a/mods/ts/sequences/infantry.yaml b/mods/ts/sequences/infantry.yaml
index ae5f6e5197..f33237d841 100644
--- a/mods/ts/sequences/infantry.yaml
+++ b/mods/ts/sequences/infantry.yaml
@@ -359,10 +359,18 @@ jumpjet:
prone-stand:
Facings: 8
ShadowStart: 451
+ flying:
+ Facings: 8
+ Length: 6
+ Start: 292
+ ShadowStart: 743
die-twirling: # TODO: animation for falling from sky starts at 436
Start: 445
Length: 6
ShadowStart: 896
+ die-falling:
+ Start: 436
+ Length: 9
die-flying: # TODO: animation for falling from sky starts at 436
Start: 445
Length: 6
@@ -379,6 +387,11 @@ jumpjet:
Length: 6
Facings: 8
ShadowStart: 615
+ flying-attack:
+ Start: 388
+ Facings: 8
+ Length: 6
+ ShadowStart: 839
prone-attack:
Start: 212
Length: 6
diff --git a/mods/ts/tilesets/snow.yaml b/mods/ts/tilesets/snow.yaml
index 0b1abe0d92..7810b74277 100644
--- a/mods/ts/tilesets/snow.yaml
+++ b/mods/ts/tilesets/snow.yaml
@@ -75,6 +75,9 @@ Terrain:
TerrainType@Subterranean:
Type: Subterranean
Color: C7C9FA
+ TerrainType@Jumpjet:
+ Type: Jumpjet
+ Color: C7C9FA
# Automatically generated. DO NOT EDIT!
Templates:
diff --git a/mods/ts/tilesets/temperate.yaml b/mods/ts/tilesets/temperate.yaml
index bb92bce8df..0a667dfb10 100644
--- a/mods/ts/tilesets/temperate.yaml
+++ b/mods/ts/tilesets/temperate.yaml
@@ -75,6 +75,9 @@ Terrain:
TerrainType@Subterranean:
Type: Subterranean
Color: 745537
+ TerrainType@Jumpjet:
+ Type: Jumpjet
+ Color: 745537
# Automatically generated. DO NOT EDIT!
Templates: