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: