diff --git a/OpenRA.Mods.RA/ParaDrop.cs b/OpenRA.Mods.RA/ParaDrop.cs index ab6af54edb..6c56c0fbae 100644 --- a/OpenRA.Mods.RA/ParaDrop.cs +++ b/OpenRA.Mods.RA/ParaDrop.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,7 @@ */ #endregion +using System; using System.Collections.Generic; using OpenRA.Mods.RA.Activities; using OpenRA.Mods.RA.Air; @@ -16,48 +17,71 @@ using OpenRA.Traits; namespace OpenRA.Mods.RA { - public class ParaDropInfo : TraitInfo + public class ParaDropInfo : ITraitInfo, Requires { - public readonly int LZRange = 4; + [Desc("Distance around the drop-point to unload troops")] + public readonly WRange DropRange = WRange.FromCells(4); + + [Desc("Sound to play when dropping")] public readonly string ChuteSound = "chute1.aud"; + + public object Create(ActorInitializer init) { return new ParaDrop(init.self, this); } } - public class ParaDrop : ITick + public class ParaDrop : ITick, INotifyRemovedFromWorld { + readonly ParaDropInfo info; + readonly Actor self; + readonly Cargo cargo; + readonly HashSet droppedAt = new HashSet(); + + public event Action OnRemovedFromWorld = self => { }; + public event Action OnEnteredDropRange = self => { }; + public event Action OnExitedDropRange = self => { }; + + [Sync] bool inDropRange; + [Sync] Target target; + bool checkForSuitableCell; - readonly List droppedAt = new List(); - CPos lz; + + public ParaDrop(Actor self, ParaDropInfo info) + { + this.info = info; + this.self = self; + cargo = self.Trait(); + } public void SetLZ(CPos lz, bool checkLandingCell) { - this.lz = lz; droppedAt.Clear(); + target = Target.FromCell(self.World, lz); checkForSuitableCell = checkLandingCell; } public void Tick(Actor self) { - var info = self.Info.Traits.Get(); - var r = info.LZRange; + var wasInDropRange = inDropRange; + inDropRange = target.IsInRange(self.CenterPosition, info.DropRange); - if ((self.Location - lz).LengthSquared <= r * r && !droppedAt.Contains(self.Location)) - { - var cargo = self.Trait(); - if (cargo.IsEmpty(self)) - FinishedDropping(self); - else - { - if (checkForSuitableCell && !IsSuitableCell(cargo.Peek(self), self.Location)) - return; + if (inDropRange && !wasInDropRange) + OnEnteredDropRange(self); - // unload a dude here - droppedAt.Add(self.Location); + if (!inDropRange && wasInDropRange) + OnExitedDropRange(self); - var a = cargo.Unload(self); - self.World.AddFrameEndTask(w => w.Add(new Parachute(a, self.CenterPosition))); - Sound.Play(info.ChuteSound, self.CenterPosition); - } - } + // Are we able to drop the next trooper? + if (!inDropRange || cargo.IsEmpty(self)) + return; + + if (droppedAt.Contains(self.Location) || checkForSuitableCell && !IsSuitableCell(cargo.Peek(self), self.Location)) + return; + + // unload a dude here + droppedAt.Add(self.Location); + + var a = cargo.Unload(self); + self.World.AddFrameEndTask(w => w.Add(new Parachute(a, self.CenterPosition))); + Sound.Play(info.ChuteSound, self.CenterPosition); } static bool IsSuitableCell(Actor actorToDrop, CPos p) @@ -65,11 +89,9 @@ namespace OpenRA.Mods.RA return actorToDrop.Trait().CanEnterCell(p); } - static void FinishedDropping(Actor self) + public void RemovedFromWorld(Actor self) { - self.CancelActivity(); - self.QueueActivity(new FlyOffMap()); - self.QueueActivity(new RemoveSelf()); + OnRemovedFromWorld(self); } } } diff --git a/OpenRA.Mods.RA/SupportPowers/ParatroopersPower.cs b/OpenRA.Mods.RA/SupportPowers/ParatroopersPower.cs index 46f13bc778..a0358cd46d 100644 --- a/OpenRA.Mods.RA/SupportPowers/ParatroopersPower.cs +++ b/OpenRA.Mods.RA/SupportPowers/ParatroopersPower.cs @@ -8,8 +8,12 @@ */ #endregion +using System; +using System.Collections.Generic; +using System.Linq; using OpenRA.Mods.RA.Activities; using OpenRA.Mods.RA.Air; +using OpenRA.Mods.RA.Effects; using OpenRA.Primitives; using OpenRA.Traits; @@ -18,18 +22,33 @@ namespace OpenRA.Mods.RA public class ParatroopersPowerInfo : SupportPowerInfo { [ActorReference] - public string[] DropItems = { }; - [ActorReference] - public string UnitType = "badr"; - [ActorReference] - public string FlareType = "flare"; + public readonly string UnitType = "badr"; + public readonly int SquadSize = 1; + public readonly WVec SquadOffset = new WVec(-1536, 1536, 0); - [Desc("In game ticks. Default value equates to 2 minutes.")] - public readonly int FlareTime = 25 * 60 * 2; + [Desc("Number of facings that the delivery aircraft may approach from.")] + public readonly int QuantizedFacings = 32; + + [Desc("Spawn and remove the plane this far outside the map.")] + public readonly WRange Cordon = new WRange(5120); + + [ActorReference] + [Desc("Troops to be delivered. They will be distributed between the planes if SquadSize > 1.")] + public string[] DropItems = { }; [Desc("Risks stuck units when they don't have the Paratrooper trait.")] public readonly bool AllowImpassableCells = false; + [ActorReference] + [Desc("Actor to spawn when the paradrop starts.")] + public readonly string CameraActor = null; + + [Desc("Amount of time (in ticks) to keep the camera alive while the passengers drop.")] + public readonly int CameraRemoveDelay = 85; + + [Desc("Weapon range offset to apply during the beacon clock calculation.")] + public readonly WRange BeaconDistanceOffset = WRange.FromCells(4); + public override object Create(ActorInitializer init) { return new ParatroopersPower(init.self, this); } } @@ -41,40 +60,126 @@ namespace OpenRA.Mods.RA { base.Activate(self, order, manager); - var info = (ParatroopersPowerInfo)Info; - var items = info.DropItems; - var startPos = self.World.Map.ChooseRandomEdgeCell(self.World.SharedRandom); + var info = Info as ParatroopersPowerInfo; + var dropFacing = Util.QuantizeFacing(self.World.SharedRandom.Next(256), info.QuantizedFacings) * (256 / info.QuantizedFacings); + var dropRotation = WRot.FromFacing(dropFacing); + var delta = new WVec(0, -1024, 0).Rotate(dropRotation); + + var altitude = self.World.Map.Rules.Actors[info.UnitType].Traits.Get().CruiseAltitude.Range; + var target = self.World.Map.CenterOfCell(order.TargetLocation) + new WVec(0, 0, altitude); + var startEdge = target - (self.World.Map.DistanceToEdge(target, -delta) + info.Cordon).Range * delta / 1024; + var finishEdge = target + (self.World.Map.DistanceToEdge(target, delta) + info.Cordon).Range * delta / 1024; + + Actor camera = null; + Beacon beacon = null; + var aircraftInRange = new Dictionary(); + + Action onEnterRange = a => + { + // Spawn a camera and remove the beacon when the first plane enters the target area + if (info.CameraActor != null && !aircraftInRange.Any(kv => kv.Value)) + { + self.World.AddFrameEndTask(w => + { + camera = w.CreateActor(info.CameraActor, new TypeDictionary + { + new LocationInit(order.TargetLocation), + new OwnerInit(self.Owner), + }); + }); + } + + if (beacon != null) + { + self.World.AddFrameEndTask(w => + { + w.Remove(beacon); + beacon = null; + }); + } + + aircraftInRange[a] = true; + }; + + Action onExitRange = a => + { + aircraftInRange[a] = false; + + // Remove the camera when the final plane leaves the target area + if (!aircraftInRange.Any(kv => kv.Value)) + { + if (camera != null) + { + camera.QueueActivity(new Wait(info.CameraRemoveDelay)); + camera.QueueActivity(new RemoveSelf()); + } + + camera = null; + } + }; self.World.AddFrameEndTask(w => { - var flare = info.FlareType != null ? w.CreateActor(info.FlareType, new TypeDictionary - { - new LocationInit(order.TargetLocation), - new OwnerInit(self.Owner), - }) : null; + var notification = self.Owner.IsAlliedWith(self.World.RenderPlayer) ? Info.LaunchSound : Info.IncomingSound; + Sound.Play(notification); - if (flare != null) + Actor distanceTestActor = null; + + var passengersPerPlane = (info.DropItems.Length + info.SquadSize - 1) / info.SquadSize; + var added = 0; + for (var i = -info.SquadSize / 2; i <= info.SquadSize / 2; i++) { - flare.QueueActivity(new Wait(info.FlareTime)); - flare.QueueActivity(new RemoveSelf()); + // Even-sized squads skip the lead plane + if (i == 0 && (info.SquadSize & 1) == 0) + continue; + + // Includes the 90 degree rotation between body and world coordinates + var so = info.SquadOffset; + var spawnOffset = new WVec(i * so.Y, -Math.Abs(i) * so.X, 0).Rotate(dropRotation); + var targetOffset = new WVec(i * so.Y, 0, 0).Rotate(dropRotation); + + var a = w.CreateActor(info.UnitType, new TypeDictionary + { + new CenterPositionInit(startEdge + spawnOffset), + new OwnerInit(self.Owner), + new FacingInit(dropFacing), + }); + + var drop = a.Trait(); + drop.SetLZ(w.Map.CellContaining(target + targetOffset), !info.AllowImpassableCells); + drop.OnEnteredDropRange += onEnterRange; + drop.OnExitedDropRange += onExitRange; + drop.OnRemovedFromWorld += onExitRange; + + var cargo = a.Trait(); + var passengers = info.DropItems.Skip(added).Take(passengersPerPlane); + added += passengersPerPlane; + + foreach (var p in passengers) + cargo.Load(a, self.World.CreateActor(false, p.ToLowerInvariant(), + new TypeDictionary { new OwnerInit(a.Owner) })); + + a.QueueActivity(new Fly(a, Target.FromPos(finishEdge + spawnOffset))); + a.QueueActivity(new RemoveSelf()); + aircraftInRange.Add(a, false); + distanceTestActor = a; } - var altitude = self.World.Map.Rules.Actors[info.UnitType].Traits.Get().CruiseAltitude; - var a = w.CreateActor(info.UnitType, new TypeDictionary + if (Info.DisplayBeacon) { - new CenterPositionInit(w.Map.CenterOfCell(startPos) + new WVec(WRange.Zero, WRange.Zero, altitude)), - new OwnerInit(self.Owner), - new FacingInit(w.Map.FacingBetween(startPos, order.TargetLocation, 0)) - }); + var distance = (target - startEdge).HorizontalLength; - a.CancelActivity(); - a.QueueActivity(new FlyAttack(Target.FromOrder(self.World, order))); - a.Trait().SetLZ(order.TargetLocation, !info.AllowImpassableCells); + beacon = new Beacon( + order.Player, + self.World.Map.CenterOfCell(order.TargetLocation), + Info.BeaconPalettePrefix, + Info.BeaconPoster, + Info.BeaconPosterPalette, + () => 1 - ((distanceTestActor.CenterPosition - target).HorizontalLength - info.BeaconDistanceOffset.Range) * 1f / distance + ); - var cargo = a.Trait(); - foreach (var i in items) - cargo.Load(a, self.World.CreateActor(false, i.ToLowerInvariant(), - new TypeDictionary { new OwnerInit(a.Owner) })); + w.Add(beacon); + } }); } } diff --git a/OpenRA.Utility/UpgradeRules.cs b/OpenRA.Utility/UpgradeRules.cs index db85cd44a8..e6ad3bd9ca 100644 --- a/OpenRA.Utility/UpgradeRules.cs +++ b/OpenRA.Utility/UpgradeRules.cs @@ -282,9 +282,9 @@ namespace OpenRA.Utility node.Key = "ParachuteSequence"; } - // SpyPlanePower was removed (use AirstrikePower instead) if (engineVersion < 20140707) { + // SpyPlanePower was removed (use AirstrikePower instead) if (depth == 1 && node.Key == "SpyPlanePower") { node.Key = "AirstrikePower"; @@ -301,6 +301,12 @@ namespace OpenRA.Utility node.Value.Nodes.Add(new MiniYamlNode("CameraRemoveDelay", new MiniYaml(revealTime.ToString()))); node.Value.Nodes.Add(new MiniYamlNode("UnitType", new MiniYaml("u2"))); } + + if (depth == 2 && node.Key == "LZRange" && parentKey == "ParaDrop") + { + node.Key = "DropRange"; + ConvertFloatToRange(ref node.Value.Value); + } } UpgradeActorRules(engineVersion, ref node.Value.Nodes, node, depth + 1); diff --git a/mods/d2k/rules/aircraft.yaml b/mods/d2k/rules/aircraft.yaml index 5199d9e902..16e71adb7d 100644 --- a/mods/d2k/rules/aircraft.yaml +++ b/mods/d2k/rules/aircraft.yaml @@ -32,7 +32,7 @@ FRIGATE: ParaDrop: - LZRange: 1 + DropRange: 1c0 Inherits: ^Plane Tooltip: Name: Frigate @@ -121,7 +121,7 @@ ORNI.bomber: CARRYALL.infantry: ParaDrop: - LZRange: 5 + DropRange: 5c0 ChuteSound: Inherits: ^Plane Health: @@ -152,7 +152,7 @@ CARRYALL.infantry: BADR: Inherits: CARRYALL.infantry ParaDrop: - LZRange: 4 + DropRange: 4c0 Tooltip: Name: Crate Carryall LeavesHusk: diff --git a/mods/ra/bits/lores-pinficon.shp b/mods/ra/bits/lores-pinficon.shp new file mode 100644 index 0000000000..def7c30575 Binary files /dev/null and b/mods/ra/bits/lores-pinficon.shp differ diff --git a/mods/ra/rules/aircraft.yaml b/mods/ra/rules/aircraft.yaml index ca6ea973ac..beea42f9e1 100644 --- a/mods/ra/rules/aircraft.yaml +++ b/mods/ra/rules/aircraft.yaml @@ -1,6 +1,6 @@ BADR: ParaDrop: - LZRange: 4 + DropRange: 4c0 Inherits: ^Plane Health: HP: 300 diff --git a/mods/ra/rules/misc.yaml b/mods/ra/rules/misc.yaml index b6bc7b3d11..82abbb1ebe 100644 --- a/mods/ra/rules/misc.yaml +++ b/mods/ra/rules/misc.yaml @@ -140,6 +140,17 @@ CAMERA: DetectCloaked: Range: 10 +camera.paradrop: + Immobile: + OccupiesSpace: false + Health: + HP: 1000 + RevealsShroud: + Range: 6c0 + ProximityCaptor: + Types: Camera + BodyOrientation: + FLARE: Immobile: OccupiesSpace: false diff --git a/mods/ra/rules/structures.yaml b/mods/ra/rules/structures.yaml index a672f2b043..f19ec9e331 100644 --- a/mods/ra/rules/structures.yaml +++ b/mods/ra/rules/structures.yaml @@ -901,7 +901,7 @@ AFLD: CameraRemoveDelay: 150 UnitType: u2 QuantizedFacings: 8 - DisplayBeacon: True + DisplayBeacon: true BeaconPoster: camicon ParatroopersPower: Icon: paratroopers @@ -911,6 +911,10 @@ AFLD: DropItems: E1,E1,E1,E3,E3 SelectTargetSound: slcttgt1.aud AllowImpassableCells: false + QuantizedFacings: 8 + CameraActor: camera.paradrop + DisplayBeacon: true + BeaconPoster: pinficon ProductionBar: SupportPowerChargeBar: PrimaryBuilding: diff --git a/mods/ra/rules/world.yaml b/mods/ra/rules/world.yaml index 441c3a3f3c..bba7700680 100644 --- a/mods/ra/rules/world.yaml +++ b/mods/ra/rules/world.yaml @@ -21,6 +21,7 @@ World: Bridges: bridge1, bridge2, br1, br2, br3, sbridge1, sbridge2, sbridge3, sbridge4 CrateSpawner: DeliveryAircraft: badr + QuantizedFacings: 16 Minimum: 1 Maximum: 3 SpawnInterval: 120 diff --git a/mods/ra/sequences/misc.yaml b/mods/ra/sequences/misc.yaml index 09c34989e3..f282d06027 100644 --- a/mods/ra/sequences/misc.yaml +++ b/mods/ra/sequences/misc.yaml @@ -137,6 +137,10 @@ beacon: Start: 0 Length: * Offset: 0,-42 + pinficon: lores-pinficon + Start: 0 + Length: * + Offset: 0,-42 clock: beaconclock Start: 0 Length: *