diff --git a/OpenRA.Mods.Common/Effects/Missile.cs b/OpenRA.Mods.Common/Effects/Missile.cs index c82d959a6b..4f23389e25 100644 --- a/OpenRA.Mods.Common/Effects/Missile.cs +++ b/OpenRA.Mods.Common/Effects/Missile.cs @@ -22,18 +22,29 @@ namespace OpenRA.Mods.Common.Effects { public class MissileInfo : IProjectileInfo { + [Desc("Name of the image containing the projectile sequence.")] public readonly string Image = null; + + [Desc("Projectile sequence name.")] [SequenceReference("Image")] public readonly string Sequence = "idle"; + [Desc("Palette used to render the projectile sequence.")] [PaletteReference] public readonly string Palette = "effect"; + [Desc("Should the projectile's shadow be rendered?")] public readonly bool Shadow = false; [Desc("Projectile speed in WDist / tick")] - public readonly WDist Speed = new WDist(8); + public readonly WDist InitialSpeed = new WDist(8); - [Desc("Maximum vertical pitch when changing altitude.")] - public readonly WAngle MaximumPitch = WAngle.FromDegrees(30); + [Desc("Vertical launch angle (pitch).")] + public readonly WAngle LaunchAngle = WAngle.Zero; + + [Desc("Maximum projectile speed in WDist / tick")] + public readonly WDist MaximumSpeed = new WDist(512); + + [Desc("Projectile acceleration when propulsion activated.")] + public readonly WDist Acceleration = WDist.Zero; [Desc("How many ticks before this missile is armed and can explode.")] public readonly int Arm = 0; @@ -47,29 +58,59 @@ namespace OpenRA.Mods.Common.Effects [Desc("Probability of locking onto and following target.")] public readonly int LockOnProbability = 100; - [Desc("In n/256 per tick.")] - public readonly int RateOfTurn = 5; + [Desc("Horizontal rate of turn.")] + public readonly int HorizontalRateOfTurn = 5; - [Desc("Explode when following the target longer than this many ticks.")] + [Desc("Vertical rate of turn.")] + public readonly int VerticalRateOfTurn = 5; + + [Desc("Run out of fuel after being activated this many ticks. Zero for unlimited fuel.")] public readonly int RangeLimit = 0; - [Desc("Trail animation.")] - public readonly string Trail = null; + [Desc("Explode when running out of fuel.")] + public readonly bool ExplodeWhenEmpty = true; - [Desc("Interval in ticks between each spawned Trail animation.")] - public readonly int TrailInterval = 2; + [Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")] + public readonly WDist AirburstAltitude = WDist.Zero; + [Desc("Cruise altitude. Zero means no cruise altitude used.")] + public readonly WDist CruiseAltitude = new WDist(512); + + [Desc("Activate homing mechanism after this many ticks.")] + public readonly int HomingActivationDelay = 0; + + [Desc("Image that contains the trail animation.")] + public readonly string TrailImage = null; + + [Desc("Smoke sequence name.")] + [SequenceReference("Trail")] public readonly string TrailSequence = "idle"; + + [Desc("Palette used to render the smoke sequence.")] [PaletteReference("TrailUsePlayerPalette")] public readonly string TrailPalette = "effect"; + + [Desc("Use the Player Palette to render the smoke sequence.")] public readonly bool TrailUsePlayerPalette = false; + [Desc("Interval in ticks between spawning smoke animation.")] + public readonly int TrailInterval = 2; + + [Desc("Should smoke animation be spawned when the propulsion is not activated.")] + public readonly bool TrailWhenDeactivated = false; + public readonly int ContrailLength = 0; + public readonly Color ContrailColor = Color.White; + public readonly bool ContrailUsePlayerColor = false; + public readonly int ContrailDelay = 1; [Desc("Should missile targeting be thrown off by nearby actors with JamsMissiles.")] public readonly bool Jammable = true; + [Desc("Range of facings by which jammed missiles can stray from current path.")] + public readonly int JammedDiversionRange = 20; + [Desc("Explodes when leaving the following terrain type, e.g., Water for torpedoes.")] public readonly string BoundToTerrainType = ""; @@ -87,12 +128,19 @@ namespace OpenRA.Mods.Common.Effects readonly ProjectileArgs args; readonly Animation anim; + readonly WVec gravity = new WVec(0, 0, -10); + int ticksToNextSmoke; ContrailRenderable contrail; string trailPalette; + int terrainHeight; [Sync] WPos pos; - [Sync] int facing; + [Sync] WVec velocity; + [Sync] int hFacing; + [Sync] int vFacing; + [Sync] bool activated; + [Sync] int speed; [Sync] WPos targetPosition; [Sync] WVec offset; @@ -109,7 +157,13 @@ namespace OpenRA.Mods.Common.Effects this.args = args; pos = args.Source; - facing = args.Facing; + hFacing = args.Facing; + vFacing = info.LaunchAngle.Angle / 4; + + speed = info.InitialSpeed.Length; + velocity = new WVec(WDist.Zero, -info.InitialSpeed, WDist.Zero) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); targetPosition = args.PassiveTarget; @@ -126,8 +180,8 @@ namespace OpenRA.Mods.Common.Effects if (!string.IsNullOrEmpty(info.Image)) { - anim = new Animation(world, info.Image, () => facing); - anim.PlayRepeating("idle"); + anim = new Animation(world, info.Image, () => hFacing); + anim.PlayRepeating(info.Sequence); } if (info.ContrailLength > 0) @@ -158,52 +212,144 @@ namespace OpenRA.Mods.Common.Effects if (anim != null) anim.Tick(); - // Missile tracks target + var cell = world.Map.CellContaining(pos); + terrainHeight = world.Map.MapHeight.Value[cell] * 512; + + // Switch from freefall mode to homing mode + if (ticks == info.HomingActivationDelay + 1) + { + activated = true; + hFacing = OpenRA.Traits.Util.GetFacing(velocity, hFacing); + speed = velocity.Length; + } + + // Switch from homing mode to freefall mode + if (info.RangeLimit != 0 && ticks == info.RangeLimit + 1) + { + activated = false; + velocity = new WVec(0, -speed, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); + } + + // Check if target position should be updated (actor visible & locked on) if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn) - targetPosition = args.GuidedTarget.CenterPosition; + targetPosition = args.GuidedTarget.CenterPosition + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); + // Compute current distance from target position var dist = targetPosition + offset - pos; - var desiredFacing = OpenRA.Traits.Util.GetFacing(dist, facing); - var desiredAltitude = targetPosition.Z; - var jammed = info.Jammable && world.ActorsWithTrait().Any(JammedBy); + var len = dist.Length; + var hLenCurr = dist.HorizontalLength; - if (jammed) + WVec move; + if (activated) { - desiredFacing = facing + world.SharedRandom.Next(-20, 21); - desiredAltitude = world.SharedRandom.Next(-43, 86); + // If target is within range, keep speed constant and aim for the target. + // The speed needs to be kept constant to keep range computation relatively simple. + + // If target is not within range, accelerate the projectile. If cruise altitudes + // are not used, aim for the target. If the cruise altitudes are used, aim for the + // target horizontally and for cruise altitude vertically. + + // Target is considered in range if after an additional tick of accelerated motion + // the horizontal distance from the target would be less than + // the diameter of the circle that the missile travels along when + // turning vertically at the maximum possible rate. + // This should work because in the worst case, the missile will have to make + // a semi-loop before hitting the target. + + // Get underestimate of distance from target in next tick, so that inRange would + // become true a little sooner than the theoretical "in range" condition is met. + var hLenNext = (long)(hLenCurr - speed - info.Acceleration.Length).Clamp(0, hLenCurr); + + // Check if target in range + bool inRange = hLenNext * hLenNext * info.VerticalRateOfTurn * info.VerticalRateOfTurn * 314 * 314 + <= 2L * 2 * speed * speed * 128 * 128 * 100 * 100; + + // Basically vDist is the representation in the x-y plane + // of the projection of dist in the z-hDist plane, + // where hDist is the projection of dist in the x-y plane. + + // This allows applying vertical rate of turn in the same way as the + // horizontal rate of turn is applied. + WVec vDist; + if (inRange || info.CruiseAltitude.Length == 0) + vDist = new WVec(-dist.Z, -hLenCurr, 0); + else + vDist = new WVec(-(dist.Z - targetPosition.Z + info.CruiseAltitude.Length + terrainHeight), -speed, 0); + + // Accelerate if out of range + if (!inRange) + speed = (speed + info.Acceleration.Length).Clamp(0, info.MaximumSpeed.Length); + + // Compute which direction the projectile should be facing + var desiredHFacing = OpenRA.Traits.Util.GetFacing(dist, hFacing); + var desiredVFacing = OpenRA.Traits.Util.GetFacing(vDist, vFacing); + + // Check whether the homing mechanism is jammed + var jammed = info.Jammable && world.ActorsWithTrait().Any(JammedBy); + if (jammed) + { + desiredHFacing = hFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1); + desiredVFacing = vFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1); + } + else if (!args.GuidedTarget.IsValidFor(args.SourceActor)) + desiredHFacing = hFacing; + + // Compute new direction the projectile will be facing + hFacing = OpenRA.Traits.Util.TickFacing(hFacing, desiredHFacing, info.HorizontalRateOfTurn); + vFacing = OpenRA.Traits.Util.TickFacing(vFacing, desiredVFacing, info.VerticalRateOfTurn); + + // Compute the projectile's guided displacement + move = new WVec(0, -1024 * speed, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))) + / 1024; } - else if (!args.GuidedTarget.IsValidFor(args.SourceActor)) - desiredFacing = facing; - - facing = OpenRA.Traits.Util.TickFacing(facing, desiredFacing, info.RateOfTurn); - var move = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(facing)) * info.Speed.Length / 1024; - - if (pos.Z != desiredAltitude) + else { - var delta = move.HorizontalLength * info.MaximumPitch.Tan() / 1024; - var dz = (targetPosition.Z - pos.Z).Clamp(-delta, delta); - move += new WVec(0, 0, dz); + // Compute the projectile's freefall displacement + move = velocity + gravity / 2; + velocity += gravity; + var velRatio = info.MaximumSpeed.Length * 1024 / velocity.Length; + if (velRatio < 1024) + velocity = velocity * velRatio / 1024; } - pos += move; - - if (!string.IsNullOrEmpty(info.Trail) && --ticksToNextSmoke < 0) + // When move (speed) is large, check for impact during the following next tick + // Shorten the move to have its length match the distance from the target + // and check for impact with the shortened move + var movLen = move.Length; + if (len < movLen) { - world.AddFrameEndTask(w => w.Add(new Smoke(w, pos - 3 * move / 2, info.Trail, trailPalette, info.Sequence))); + var npos = pos + move * 1024 * len / movLen / 1024; + if (world.Map.DistanceAboveTerrain(npos).Length <= 0 // Hit the ground + || (targetPosition + offset - npos).LengthSquared < info.CloseEnough.LengthSquared) // Within range + pos = npos; + else + pos += move; + } + else + pos += move; + + // Create the smoke trail effect + if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (activated || info.TrailWhenDeactivated)) + { + world.AddFrameEndTask(w => w.Add(new Smoke(w, pos - 3 * move / 2, info.TrailImage, trailPalette, info.TrailSequence))); ticksToNextSmoke = info.TrailInterval; } if (info.ContrailLength > 0) contrail.Update(pos); - var cell = world.Map.CellContaining(pos); - - var shouldExplode = (pos.Z < 0) // Hit the ground - || (dist.LengthSquared < info.CloseEnough.LengthSquared) // Within range - || (info.RangeLimit != 0 && ticks > info.RangeLimit) // Ran out of fuel + var height = world.Map.DistanceAboveTerrain(pos); + var shouldExplode = (height.Length <= 0) // Hit the ground + || (len < info.CloseEnough.Length) // Within range + || (info.ExplodeWhenEmpty && info.RangeLimit != 0 && ticks > info.RangeLimit) // Ran out of fuel || (info.Blockable && BlocksProjectiles.AnyBlockingActorAt(world, pos)) // Hit a wall or other blocking obstacle || !world.Map.Contains(cell) // This also avoids an IndexOutOfRangeException in GetTerrainInfo below. - || (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType); // Hit incompatible terrain + || (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType) // Hit incompatible terrain + || (height.Length < info.AirburstAltitude.Length && hLenCurr < info.CloseEnough.Length); // Airburst if (shouldExplode) Explode(world); diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index b022e125c8..14856bcfaa 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -2718,6 +2718,18 @@ namespace OpenRA.Mods.Common.UtilityCommands } } + if (engineVersion < 20150912) + { + if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "Speed") + node.Key = "InitialSpeed"; + + if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "RateOfTurn") + node.Key = "HorizontalRateOfTurn"; + + if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "Trail") + node.Key = "TrailImage"; + } + UpgradeWeaponRules(engineVersion, ref node.Value.Nodes, node, depth + 1); } }