diff --git a/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs b/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs new file mode 100644 index 0000000000..56f7e1b137 --- /dev/null +++ b/OpenRA.Mods.Cnc/Projectiles/DropPodImpact.cs @@ -0,0 +1,78 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.Linq; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Effects +{ + public class DropPodImpact : IProjectile + { + readonly Target target; + readonly Animation entryAnimation; + readonly Player firedBy; + readonly string entryPalette; + readonly WeaponInfo weapon; + readonly WPos launchPos; + + int weaponDelay; + bool impacted = false; + + public DropPodImpact(Player firedBy, WeaponInfo weapon, World world, WPos launchPos, Target target, + int delay, string entryEffect, string entrySequence, string entryPalette) + { + this.target = target; + this.firedBy = firedBy; + this.weapon = weapon; + this.entryPalette = entryPalette; + weaponDelay = delay; + this.launchPos = launchPos; + + entryAnimation = new Animation(world, entryEffect); + entryAnimation.PlayThen(entrySequence, () => Finish(world)); + + if (weapon.Report != null && weapon.Report.Any()) + Game.Sound.Play(SoundType.World, weapon.Report, world, launchPos); + } + + public void Tick(World world) + { + entryAnimation.Tick(); + + if (!impacted && weaponDelay-- <= 0) + { + var warheadArgs = new WarheadArgs + { + Weapon = weapon, + Source = target.CenterPosition, + SourceActor = firedBy.PlayerActor, + WeaponTarget = target + }; + + weapon.Impact(target, warheadArgs); + impacted = true; + } + } + + public IEnumerable Render(WorldRenderer wr) + { + return entryAnimation.Render(launchPos, wr.Palette(entryPalette)); + } + + void Finish(World world) + { + world.AddFrameEndTask(w => w.Remove(this)); + } + } +} diff --git a/OpenRA.Mods.Cnc/Traits/SupportPowers/DropPodsPower.cs b/OpenRA.Mods.Cnc/Traits/SupportPowers/DropPodsPower.cs new file mode 100644 index 0000000000..f02baf607f --- /dev/null +++ b/OpenRA.Mods.Cnc/Traits/SupportPowers/DropPodsPower.cs @@ -0,0 +1,159 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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.GameRules; +using OpenRA.Mods.Cnc.Effects; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.Cnc.Traits +{ + public class DropPodsPowerInfo : SupportPowerInfo, IRulesetLoaded + { + [FieldLoader.Require] + [Desc("Drop pod unit")] + [ActorReference(typeof(AircraftInfo), typeof(FallsToEarthInfo))] + public readonly string[] UnitTypes = null; + + [Desc("Number of drop pods spawned.")] + public readonly int2 Drops = new int2(5, 8); + + [Desc("Sets the approach direction.")] + public readonly int PodFacing = 32; + + [Desc("Maximum offset from targetLocation")] + public readonly int PodScatter = 3; + + [Desc("Effect sequence sprite image")] + public readonly string EntryEffect = "podring"; + + [Desc("Effect sequence to display in the air.")] + [SequenceReference("Effect")] + public readonly string EntryEffectSequence = "idle"; + + [PaletteReference] + public readonly string EntryEffectPalette = "effect"; + + [ActorReference] + [Desc("Actor to spawn when the attack starts")] + public readonly string CameraActor = null; + + [Desc("Number of ticks to keep the camera alive")] + public readonly int CameraRemoveDelay = 25; + + [Desc("Which weapon to fire")] + [WeaponReference] + public readonly string Weapon = "Vulcan2"; + + public WeaponInfo WeaponInfo { get; private set; } + + [Desc("Apply the weapon impact this many ticks into the effect")] + public readonly int WeaponDelay = 0; + + public override object Create(ActorInitializer init) { return new DropPodsPower(init.Self, this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + WeaponInfo weapon; + var weaponToLower = (Weapon ?? string.Empty).ToLowerInvariant(); + if (!rules.Weapons.TryGetValue(weaponToLower, out weapon)) + throw new YamlException("Weapons Ruleset does not contain an entry '{0}'".F(weaponToLower)); + + WeaponInfo = weapon; + + base.RulesetLoaded(rules, ai); + } + } + + public class DropPodsPower : SupportPower + { + readonly DropPodsPowerInfo info; + + public DropPodsPower(Actor self, DropPodsPowerInfo info) + : base(self, info) + { + this.info = info; + } + + public override void Activate(Actor self, Order order, SupportPowerManager manager) + { + base.Activate(self, order, manager); + + SendDropPods(self, order, info.PodFacing); + } + + public void SendDropPods(Actor self, Order order, int podFacing) + { + var actorInfo = self.World.Map.Rules.Actors[info.UnitTypes.First().ToLowerInvariant()]; + var aircraftInfo = actorInfo.TraitInfo(); + var altitude = aircraftInfo.CruiseAltitude.Length; + var approachRotation = WRot.FromFacing(podFacing); + var fallsToEarthInfo = actorInfo.TraitInfo(); + var delta = new WVec(0, -altitude * aircraftInfo.Speed / fallsToEarthInfo.Velocity.Length, 0).Rotate(approachRotation); + + self.World.AddFrameEndTask(w => + { + var target = order.Target.CenterPosition; + var targetCell = self.World.Map.CellContaining(target); + var podLocations = self.World.Map.FindTilesInCircle(targetCell, info.PodScatter) + .Where(c => aircraftInfo.LandableTerrainTypes.Contains(w.Map.GetTerrainInfo(c).Type) + && !self.World.ActorMap.GetActorsAt(c).Any()); + + if (!podLocations.Any()) + return; + + if (info.CameraActor != null) + { + var camera = w.CreateActor(info.CameraActor, new TypeDictionary + { + new LocationInit(targetCell), + new OwnerInit(self.Owner), + }); + + camera.QueueActivity(new Wait(info.CameraRemoveDelay)); + camera.QueueActivity(new RemoveSelf()); + } + + PlayLaunchSounds(); + + var drops = self.World.SharedRandom.Next(info.Drops.X, info.Drops.Y); + for (var i = 0; i < drops; i++) + { + var unitType = info.UnitTypes.Random(self.World.SharedRandom); + var podLocation = podLocations.Random(self.World.SharedRandom); + var podTarget = Target.FromCell(w, podLocation); + var location = self.World.Map.CenterOfCell(podLocation) - delta + new WVec(0, 0, altitude); + + var pod = w.CreateActor(false, unitType, new TypeDictionary + { + new CenterPositionInit(location), + new OwnerInit(self.Owner), + new FacingInit(podFacing) + }); + + var aircraft = pod.Trait(); + if (!aircraft.CanLand(podLocation)) + pod.Dispose(); + else + { + w.Add(new DropPodImpact(self.Owner, info.WeaponInfo, w, location, podTarget, info.WeaponDelay, + info.EntryEffect, info.EntryEffectSequence, info.EntryEffectPalette)); + w.Add(pod); + } + } + }); + } + } +} diff --git a/mods/ts/rules/aircraft.yaml b/mods/ts/rules/aircraft.yaml index 0b7f119ab3..a796cfaa65 100644 --- a/mods/ts/rules/aircraft.yaml +++ b/mods/ts/rules/aircraft.yaml @@ -35,6 +35,52 @@ DPOD: EmptySequence: pip-ammoempty Palette: pips +DPOD2: + Inherits@2: ^ExistsInWorld + Valued: + Cost: 10 + Tooltip: + Name: Drop Pod + Health: + HP: 6000 + Armor: + Type: Light + Aircraft: + TurnSpeed: 5 + Speed: 300 + CruiseAltitude: 16c0 + MaximumPitch: 110 + LandableTerrainTypes: Clear, Road, Rail, DirtRoad, Rough + HiddenUnderFog: + Type: CenterPosition + BodyOrientation: + UseClassicPerspectiveFudge: False + RenderSprites: + Image: pod + WithFacingSpriteBody: + QuantizeFacingsFromSequence: + HitShape: + Interactable: + WithShadow: + SmokeTrailWhenDamaged: + Sprite: largesmoke + MinDamage: Undamaged + FallsToEarth: + Explosion: DropPodExplode + Moves: true + Velocity: 768 + MaximumSpinSpeed: 0 + +DPOD2E1: + Inherits: DPOD2 + SpawnActorOnDeath: + Actor: E1R3 + +DPOD2E2: + Inherits: DPOD2 + SpawnActorOnDeath: + Actor: E2R3 + DSHP: Inherits: ^Aircraft Inherits@CARGOPIPS: ^CargoPips diff --git a/mods/ts/rules/gdi-infantry.yaml b/mods/ts/rules/gdi-infantry.yaml index cf5e2e2fb5..bcf7c8b236 100644 --- a/mods/ts/rules/gdi-infantry.yaml +++ b/mods/ts/rules/gdi-infantry.yaml @@ -33,6 +33,15 @@ E2: ProducibleWithLevel: Prerequisites: barracks.upgraded +E2R3: + Inherits: E2 + RenderSprites: + Image: E2 + ProducibleWithLevel: + Prerequisites: techlevel.low + InitialLevels: 3 + -Buildable: + MEDIC: Inherits: ^Soldier Inherits@EXPERIENCE: ^GainsExperience diff --git a/mods/ts/rules/gdi-structures.yaml b/mods/ts/rules/gdi-structures.yaml index 288091d553..a4289ede68 100644 --- a/mods/ts/rules/gdi-structures.yaml +++ b/mods/ts/rules/gdi-structures.yaml @@ -481,6 +481,18 @@ GAPLUG: SelectTargetSpeechNotification: SelectTarget DisplayRadarPing: True CameraActor: camera + DropPodsPower: + Cursor: ioncannon + PauseOnCondition: disabled || empdisable + RequiresCondition: plug.droppoda || plug.droppodb + Icon: droppods + Description: Drop Pods + LongDesc: Drop Pod reinforcements.\nSmall team of elite soldiers orbital drops\nto target location. + SelectTargetSpeechNotification: SelectTarget + DisplayRadarPing: true + ChargeInterval: 10000 + UnitTypes: DPOD2E1, DPOD2E2 + CameraActor: camera ProduceActorPower: PauseOnCondition: disabled || empdisable RequiresCondition: plug.hunterseekera || plug.hunterseekerb @@ -504,17 +516,23 @@ GAPLUG: Power@hunterseeker: RequiresCondition: plug.hunterseekera || plug.hunterseekerb Amount: -50 + Power@droppod: + RequiresCondition: plug.droppoda || plug.droppodb + Amount: -20 Pluggable@pluga: Offset: 0,2 Conditions: plug.ioncannon: plug.ioncannona plug.hunterseeker: plug.hunterseekera + plug.droppod: plug.droppoda Requirements: - plug.ioncannon: !build-incomplete && !plug.ioncannonb && !plug.ioncannona && !plug.hunterseekera - plug.hunterseeker: !build-incomplete && !plug.hunterseekerb && !plug.ioncannona && !plug.hunterseekera + plug.ioncannon: !build-incomplete && !plug.ioncannonb && !plug.ioncannona && !plug.hunterseekera && !plug.droppoda + plug.hunterseeker: !build-incomplete && !plug.hunterseekerb && !plug.ioncannona && !plug.hunterseekera && !plug.droppoda + plug.droppod: !build-incomplete && !plug.droppodb && !plug.ioncannona && !plug.hunterseekera && !plug.droppoda EditorOptions: plug.ioncannon: Ion Cannon plug.hunterseeker: Hunter Seeker + plug.droppod: Drop Pod Reinforcements WithIdleOverlay@ioncannona: RequiresCondition: !build-incomplete && plug.ioncannona PauseOnCondition: disabled @@ -523,17 +541,24 @@ GAPLUG: RequiresCondition: !build-incomplete && plug.hunterseekera PauseOnCondition: disabled Sequence: idle-hunterseekera + WithIdleOverlay@droppoda: + RequiresCondition: !build-incomplete && plug.droppoda + PauseOnCondition: disabled + Sequence: idle-droppoda Pluggable@plugb: Offset: 1,2 Conditions: plug.ioncannon: plug.ioncannonb plug.hunterseeker: plug.hunterseekerb + plug.droppod: plug.droppodb Requirements: - plug.ioncannon: !build-incomplete && !plug.ioncannona && !plug.ioncannonb && !plug.hunterseekerb - plug.hunterseeker: !build-incomplete && !plug.hunterseekera && !plug.ioncannonb && !plug.hunterseekerb + plug.ioncannon: !build-incomplete && !plug.ioncannona && !plug.ioncannonb && !plug.hunterseekerb && !plug.droppodb + plug.hunterseeker: !build-incomplete && !plug.hunterseekera && !plug.ioncannonb && !plug.hunterseekerb && !plug.droppodb + plug.droppod: !build-incomplete && !plug.droppoda && !plug.ioncannonb && !plug.hunterseekerb && !plug.droppodb EditorOptions: plug.ioncannon: Ion Cannon plug.hunterseeker: Hunter Seeker + plug.droppod: Drop Pod Reinforcements WithIdleOverlay@ioncannonb: RequiresCondition: !build-incomplete && plug.ioncannonb PauseOnCondition: disabled @@ -542,6 +567,10 @@ GAPLUG: RequiresCondition: !build-incomplete && plug.hunterseekerb PauseOnCondition: disabled Sequence: idle-hunterseekerb + WithIdleOverlay@droppodb: + RequiresCondition: plug.droppodb + PauseOnCondition: disabled + Sequence: idle-droppodb ProvidesPrerequisite@buildingname: ProvidesPrerequisite@pluggableion: RequiresCondition: !plug.ioncannona && !plug.ioncannonb @@ -595,3 +624,19 @@ GAPLUG3: Type: plug.ioncannon Power: Amount: -100 + +GAPLUG4: + Inherits: ^BuildingPlug + Valued: + Cost: 1000 + Tooltip: + Name: Drop Pod Node + Buildable: + Queue: Building + BuildPaletteOrder: 180 + Prerequisites: gaplug, gatech, ~structures.gdi, ~techlevel.superweapons + Description: Enables use of the Drop Pod Reinforcements. + Plug: + Type: plug.droppod + Power: + Amount: -20 diff --git a/mods/ts/rules/shared-infantry.yaml b/mods/ts/rules/shared-infantry.yaml index 66e02b6ff8..8f56d48cc7 100644 --- a/mods/ts/rules/shared-infantry.yaml +++ b/mods/ts/rules/shared-infantry.yaml @@ -38,6 +38,15 @@ E1: RevealsShroud: Range: 5c0 +E1R3: + Inherits: E1 + RenderSprites: + Image: e1.gdi + ProducibleWithLevel: + Prerequisites: techlevel.low + InitialLevels: 3 + -Buildable: + ENGINEER: Inherits: ^Soldier Inherits@selection: ^SelectableSupportUnit diff --git a/mods/ts/sequences/aircraft.yaml b/mods/ts/sequences/aircraft.yaml index f483981188..f9da2ad528 100644 --- a/mods/ts/sequences/aircraft.yaml +++ b/mods/ts/sequences/aircraft.yaml @@ -30,3 +30,9 @@ apache: orcatran: Inherits: ^VehicleOverlays icon: crryicon + +pod: + Inherits: ^VehicleOverlays + idle: + Facings: 1 + Length: 1 diff --git a/mods/ts/sequences/misc.yaml b/mods/ts/sequences/misc.yaml index 6a36ce28b0..c44189db75 100644 --- a/mods/ts/sequences/misc.yaml +++ b/mods/ts/sequences/misc.yaml @@ -211,6 +211,18 @@ explosion: small_grey_explosion: xgrysml2 medium_grey_explosion: xgrymed1 large_grey_explosion: xgrymed2 + droppod_explosion: droppod + Length: 8 + Tick: 360 + droppod2_explosion: droppod2 + Length: 8 + Tick: 360 + droppody_explosion: droppody + Length: 8 + Tick: 360 + droppody2_explosion: droppody2 + Length: 8 + Tick: 360 discus: idle: @@ -440,6 +452,7 @@ icon: ioncannon: ioncicon hunterseeker: detnicon emp: pulsicon + droppods: podsicon clustermissile: up: mltimisl-placeholder # TODO: use voxel @@ -655,3 +668,8 @@ typeglyphs: Start: 3 structure: Start: 4 + +podring: + idle: + Frames: 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 + Length: * diff --git a/mods/ts/sequences/structures.yaml b/mods/ts/sequences/structures.yaml index 6f4e8abba6..51a214c410 100644 --- a/mods/ts/sequences/structures.yaml +++ b/mods/ts/sequences/structures.yaml @@ -1977,6 +1977,13 @@ gaplug: Length: 15 Reverses: true Tick: 120 + idle-droppoda: gaplug_d + Length: 15 + Tick: 120 + Offset: -12, -42, 30 + idle-droppodb: gaplug_d + Length: 15 + Tick: 120 make: gtplugmk Length: 17 ShadowStart: 17 @@ -2009,3 +2016,12 @@ gaplug3: Length: 14 Tick: 120 icon: rad3icon + +gaplug4: + place: gtplug_d + Offset: 24,-48, 48 + UseTilesetCode: true + Reverses: true + Length: 14 + Tick: 120 + icon: rad1icon diff --git a/mods/ts/weapons/explosions.yaml b/mods/ts/weapons/explosions.yaml index cc8410baaf..7cfce5da78 100644 --- a/mods/ts/weapons/explosions.yaml +++ b/mods/ts/weapons/explosions.yaml @@ -66,3 +66,8 @@ Demolish: Explosions: large_twlt ExplosionPalette: effect-ignore-lighting-alpha75 ImpactSounds: expnew09.aud + +DropPodExplode: + Warhead@1Eff: CreateEffect + Explosions: droppod_explosion, droppod2_explosion, droppody_explosion, droppody2_explosion + ExplosionPalette: effect-ignore-lighting-alpha75