From 41f57f2a15d5e90347247fb8597d6b78d176c716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Husti=C4=87?= Date: Tue, 14 Jul 2015 20:10:56 +0100 Subject: [PATCH 1/4] Missile projectile refactor. Introduces: Vertical rate of turn Customizable vertical launch angle Delayed activation of the homing mechanism / propulsion Freefall while propulsion deactivated Customizable explosion altitude (for airburst) Customizable cruise altitude (when target out of range) Height checks for terrain impact and shadow rendering Acceleration (instead of constant speed from launch to impact) --- OpenRA.Mods.Common/Effects/Missile.cs | 230 ++++++++++++++---- .../UtilityCommands/UpgradeRules.cs | 12 + 2 files changed, 200 insertions(+), 42 deletions(-) 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); } } From b9e57d33c37f07091e51883b5d95268414fe9cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Husti=C4=87?= Date: Mon, 14 Sep 2015 23:24:05 +0200 Subject: [PATCH 2/4] Added lookahead, launch speed & angle computation. The missiles should be more intelligent, avoiding cliffs, surmounting inclines and flexibly selecting appropriate launch speed and angle to avoid a close incline or miss a close target. --- OpenRA.Mods.Common/Effects/Missile.cs | 578 ++++++++++++++---- .../UtilityCommands/UpgradeRules.cs | 4 +- 2 files changed, 450 insertions(+), 132 deletions(-) diff --git a/OpenRA.Mods.Common/Effects/Missile.cs b/OpenRA.Mods.Common/Effects/Missile.cs index 4f23389e25..97f0a18f06 100644 --- a/OpenRA.Mods.Common/Effects/Missile.cs +++ b/OpenRA.Mods.Common/Effects/Missile.cs @@ -34,17 +34,23 @@ namespace OpenRA.Mods.Common.Effects [Desc("Should the projectile's shadow be rendered?")] public readonly bool Shadow = false; - [Desc("Projectile speed in WDist / tick")] - public readonly WDist InitialSpeed = new WDist(8); + [Desc("Minimum vertical launch angle (pitch).")] + public readonly WAngle MinimumLaunchAngle = new WAngle(-128); - [Desc("Vertical launch angle (pitch).")] - public readonly WAngle LaunchAngle = WAngle.Zero; + [Desc("Maximum vertical launch angle (pitch).")] + public readonly WAngle MaximumLaunchAngle = new WAngle(64); + + [Desc("Minimum launch speed in WDist / tick")] + public readonly WDist MinimumLaunchSpeed = WDist.Zero; + + [Desc("Maximum launch speed in WDist / tick")] + public readonly WDist MaximumLaunchSpeed = new WDist(8); [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; + public readonly WDist Acceleration = new WDist(5); [Desc("How many ticks before this missile is armed and can explode.")] public readonly int Arm = 0; @@ -62,7 +68,7 @@ namespace OpenRA.Mods.Common.Effects public readonly int HorizontalRateOfTurn = 5; [Desc("Vertical rate of turn.")] - public readonly int VerticalRateOfTurn = 5; + public readonly int VerticalRateOfTurn = 6; [Desc("Run out of fuel after being activated this many ticks. Zero for unlimited fuel.")] public readonly int RangeLimit = 0; @@ -83,7 +89,7 @@ namespace OpenRA.Mods.Common.Effects public readonly string TrailImage = null; [Desc("Smoke sequence name.")] - [SequenceReference("Trail")] public readonly string TrailSequence = "idle"; + [SequenceReference("TrailImage")] public readonly string TrailSequence = "idle"; [Desc("Palette used to render the smoke sequence.")] [PaletteReference("TrailUsePlayerPalette")] public readonly string TrailPalette = "effect"; @@ -124,32 +130,47 @@ namespace OpenRA.Mods.Common.Effects public class Missile : IEffect, ISync { + enum States + { + Freefall, + Homing, + Hitting + } + readonly MissileInfo info; readonly ProjectileArgs args; readonly Animation anim; + // NOTE: Might be desirable to unhardcode the number -10 readonly WVec gravity = new WVec(0, 0, -10); + int ticks; + int ticksToNextSmoke; ContrailRenderable contrail; string trailPalette; - int terrainHeight; + + States state; + bool targetPassedBy; + bool lockOn = false; + + WPos targetPosition; + WVec offset; + + WVec tarVel; + WVec predVel; [Sync] WPos pos; - [Sync] WVec velocity; + WVec velocity; + int speed; + int loopRadius; + + int renderFacing; [Sync] int hFacing; [Sync] int vFacing; - [Sync] bool activated; - [Sync] int speed; - [Sync] WPos targetPosition; - [Sync] WVec offset; - [Sync] int ticks; - - [Sync] bool lockOn = false; - - [Sync] public Actor SourceActor { get { return args.SourceActor; } } - [Sync] public Target GuidedTarget { get { return args.GuidedTarget; } } + public Actor SourceActor { get { return args.SourceActor; } } + public Target GuidedTarget { get { return args.GuidedTarget; } } public Missile(MissileInfo info, ProjectileArgs args) { @@ -158,29 +179,28 @@ namespace OpenRA.Mods.Common.Effects pos = args.Source; 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; var world = args.SourceActor.World; - if (world.SharedRandom.Next(100) <= info.LockOnProbability) - lockOn = true; - if (info.Inaccuracy.Length > 0) { var inaccuracy = OpenRA.Traits.Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers); offset = WVec.FromPDF(world.SharedRandom, 2) * inaccuracy / 1024; } + DetermineLaunchSpeedAndAngle(world, out speed, out vFacing); + + velocity = new WVec(0, -info.MaximumLaunchSpeed.Length, 0) + .Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero)) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); + + if (world.SharedRandom.Next(100) <= info.LockOnProbability) + lockOn = true; + if (!string.IsNullOrEmpty(info.Image)) { - anim = new Animation(world, info.Image, () => hFacing); + anim = new Animation(world, info.Image, () => renderFacing); anim.PlayRepeating(info.Sequence); } @@ -195,6 +215,150 @@ namespace OpenRA.Mods.Common.Effects trailPalette += args.SourceActor.Owner.InternalName; } + static int LoopRadius(int speed, int rot) + { + // loopRadius in w-units = speed in w-units per tick / angular speed in radians per tick + // angular speed in radians per tick = VROT in facing units per tick * (pi radians / 128 facing units) + // pi = 314 / 100 + // ==> loopRadius = (speed * 128 * 100) / (314 * VROT) + return (speed * 6400) / (157 * rot); + } + + void DetermineLaunchSpeedAndAngle(World world, out int speed, out int vFacing) + { + speed = info.MaximumLaunchSpeed.Length; + var loopRadius = LoopRadius(speed, info.VerticalRateOfTurn); + + // Compute current distance from target position + var tarDistVec = targetPosition + offset - pos; + var relTarHorDist = tarDistVec.HorizontalLength; + + int predClfHgt; + int predClfDist; + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist); + + // Height difference between the incline height and missile height + var diffClfMslHgt = predClfHgt - pos.Z; + + // Incline coming up + if (diffClfMslHgt >= 0) + { + // Find smallest vertical facing, attainable in the next tick, + // for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent + // with vertical facing 0 + vFacing = info.MaximumLaunchAngle.Angle >> 2; + + // Compute minimum speed necessary to both be able to face directly upwards and + // have enough space to hit the target without passing it by (and thus having to + // do horizontal loops) + var minSpeed = ((System.Math.Min(predClfDist * 1024 / (1024 - WAngle.FromFacing(vFacing).Sin()), + (relTarHorDist + predClfDist) * 1024 / (2 * (2048 - WAngle.FromFacing(vFacing).Sin()))) + * info.VerticalRateOfTurn * 157) / 6400).Clamp(info.MinimumLaunchSpeed.Length, info.MaximumLaunchSpeed.Length); + + if ((sbyte)vFacing < 0) + speed = minSpeed; + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt) + && !WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) + { + // Find highest speed greater than the above minimum that allows the missile + // to surmount the incline + var vFac = vFacing; + speed = BisectionSearch(minSpeed, info.MaximumLaunchSpeed.Length, spd => + { + var lpRds = LoopRadius(spd, info.VerticalRateOfTurn); + return WillClimbWithinDistance(vFac, lpRds, predClfDist, diffClfMslHgt) + || WillClimbAroundInclineTop(vFac, lpRds, predClfDist, diffClfMslHgt, spd); + }); + } + else + { + // Find least vertical facing that will allow the missile to climb + // terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + vFacing = BisectionSearch(0, info.MaximumLaunchAngle.Angle, + vFac => !WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)) + 1; + } + } + else + { + // Set vertical facing so that the missile faces its target + var vDist = new WVec(-tarDistVec.Z, -relTarHorDist, 0); + vFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, 0); + + // Do not accept -1 as valid vertical facing since it is usually a numerical error + // and will lead to premature descent and crashing into the ground + if (vFacing == -1) + vFacing = 0; + + // Make sure the chosen vertical facing adheres to prescribed bounds + vFacing = vFacing.Clamp((sbyte)(info.MinimumLaunchAngle.Angle >> 2), + (sbyte)(info.MaximumLaunchAngle.Angle >> 2)); + } + } + + // Will missile be able to climb terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + // Calling this function only makes sense when vFacing is nonzero + static bool WillClimbWithinDistance(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt) + { + // Missile's horizontal distance from loop's center + var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + + // Missile's height above loop's center + var missHgt = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024; + + // Height that would be climbed without changing vertical facing + // for a horizontal distance hHeightChange - missDist + var hgtChg = (predClfDist - missDist) * WAngle.FromFacing(vFacing).Tan() / 1024; + + // Check if total manoeuvre height enough to overcome the incline's height + return hgtChg + missHgt >= diffClfMslHgt; + } + + // This function checks if the missile's vertical facing is + // nonnegative, and the incline top's horizontal distance from the missile is + // less than loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 + static bool IsNearInclineTop(int vFacing, int loopRadius, int predClfDist) + { + return vFacing >= 0 && predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024; + } + + // Will missile climb around incline top if bringing vertical facing + // down to zero on an arc of radius loopRadius + // Calling this function only makes sense when IsNearInclineTop returns true + static bool WillClimbAroundInclineTop(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt, int speed) + { + // Vector from missile's current position pointing to the loop's center + var radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(System.Math.Max(0, 64 - vFacing)))); + + // Vector from loop's center to incline top + 64 hardcoded in height buffer zone + var topVector = new WVec(predClfDist, diffClfMslHgt + 64, 0) - radius; + + // Check if incline top inside of the vertical loop + return topVector.Length <= loopRadius; + } + + static int BisectionSearch(int lowerBound, int upperBound, System.Func testCriterion) + { + // Assuming that there exists an integer N between lowerBound and upperBound + // for which testCriterion returns true as well as all integers less than N, + // and for which testCriterion returns false for all integers greater than N, + // this function finds N. + while (upperBound - lowerBound > 1) + { + var middle = (upperBound + lowerBound) / 2; + + if (testCriterion(middle)) + lowerBound = middle; + else + upperBound = middle; + } + + return lowerBound; + } + bool JammedBy(TraitPair tp) { if ((tp.Actor.CenterPosition - pos).HorizontalLengthSquared > tp.Trait.Range.LengthSquared) @@ -206,134 +370,284 @@ namespace OpenRA.Mods.Common.Effects return tp.Actor.World.SharedRandom.Next(100 / tp.Trait.Chance) == 0; } + void ChangeSpeed(int sign = 1) + { + speed = (speed + sign * info.Acceleration.Length).Clamp(0, info.MaximumSpeed.Length); + + // Compute the vertical loop radius + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn); + } + + WVec FreefallTick() + { + // Compute the projectile's freefall displacement + var move = velocity + gravity / 2; + velocity += gravity; + var velRatio = info.MaximumSpeed.Length * 1024 / velocity.Length; + if (velRatio < 1024) + velocity = velocity * velRatio / 1024; + + return move; + } + + // NOTE: It might be desirable to make lookahead more intelligent by outputting more information + // than just the highest point in the lookahead distance + void InclineLookahead(World world, int distCheck, out int predClfHgt, out int predClfDist) + { + predClfHgt = 0; // Highest probed terrain height + predClfDist = 0; // Distance from highest point + + // NOTE: Might be desired to unhardcode the lookahead step size + var stepSize = 128; + var step = new WVec(0, -stepSize, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); // Step vector of length 128 + + // Probe terrain ahead of the missile + // NOTE: Might be desired to unhardcode maximum lookahead distance + var maxLookaheadDistance = loopRadius * 4; + var posProbe = pos; + var curDist = 0; + var tickLimit = System.Math.Min(maxLookaheadDistance, distCheck) / stepSize; + for (var tick = 0; tick <= tickLimit; tick++) + { + posProbe += step; + curDist += stepSize; + var ht = world.Map.MapHeight.Value[world.Map.CellContaining(posProbe)] * 512; + if (ht > predClfHgt) + { + predClfHgt = ht; + predClfDist = curDist; + } + } + } + + int HomingInnerTick(int predClfDist, int diffClfMslHgt, int relTarHorDist, + int nxtRelTarHorDist, int relTarHgt, int vFacing, bool targetPassedBy) + { + int desiredVFacing = vFacing; + + // NOTE: Might be desired to unhardcode the distance from target + // at which the missile no longer cruises at cruise altitude + // but instead keeps trying to hit the target + // It still avoids inclines however + int targetLockonDistance = 2 * loopRadius; + + // Incline coming up -> attempt to reach the incline so that after predClfDist + // the height above the terrain is positive but as close to 0 as possible + // Also, never change horizontal facing and never travel backwards + // Possible techniques to avoid close cliffs are deceleration, turning + // as sharply as possible to travel directly upwards and then returning + // to zero vertical facing as low as possible while still not hittin the + // high terrain. A last technique (and the preferred one, normally used when + // the missile hasn't been fired near a cliff) is simply finding the smallest + // vertical facing that allows for a smooth climb to the new terrain's height + // and coming in at predClfDist at exactly zero vertical facing + if (diffClfMslHgt >= 0) + { + // If missile is below incline top height and facing downwards, bring back + // its vertical facing above zero as soon as possible + if ((sbyte)vFacing < 0) + desiredVFacing = info.VerticalRateOfTurn; + + // Missile will climb around incline top if bringing vertical facing + // down to zero on an arc of radius loopRadius + else if (IsNearInclineTop(vFacing, loopRadius, predClfDist) + && WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) + desiredVFacing = 0; + + // Missile will not climb terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt)) + + // Find smallest vertical facing, attainable in the next tick, + // for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent + // with vertical facing 0 + for (var vFac = System.Math.Min(vFacing + info.VerticalRateOfTurn - 1, 63); vFac > vFacing; vFac--) + if (!WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt) + && !(predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFac).Sin()) / 1024 + && WillClimbAroundInclineTop(vFac, loopRadius, predClfDist, diffClfMslHgt, speed))) + { + desiredVFacing = vFac + 1; + break; + } + + // Attained height after ascent as predicted from upper part of incline surmounting manoeuvre + var predAttHght = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024 - diffClfMslHgt; + + // Should the missile be slowed down in order to make it more manoeuverable + var slowDown = info.Acceleration.Length != 0 // Possible to decelerate + && ((desiredVFacing != 0 // Lower part of incline surmounting manoeuvre + + // Incline will be hit before vertical facing attains 64 + && (predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 + + // When evaluating this the incline will be *not* be hit before vertical facing attains 64 + // At current speed target too close to hit without passing it by + || relTarHorDist <= 2 * loopRadius * (2048 - WAngle.FromFacing(vFacing).Sin()) / 1024 - predClfDist)) + + || (desiredVFacing == 0 // Upper part of incline surmounting manoeuvre + && relTarHorDist <= loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024 + + Exts.ISqrt(predAttHght * (2 * loopRadius - predAttHght)))); // Target too close to hit at current speed + + if (slowDown) + ChangeSpeed(-1); + } + else if (nxtRelTarHorDist <= 2 * loopRadius || state == States.Hitting) + { + // No longer travel at cruise altitude + state = States.Hitting; + + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + + // Do not accept -1 as valid vertical facing since it is usually a numerical error + // and will lead to premature descent and crashing into the ground + if (desiredVFacing == -1) + desiredVFacing = 0; + + // If the target has been passed by, limit the absolute value of + // vertical facing by the maximum vertical rate of turn + // Do this because the missile will be looping horizontally + // and thus needs smaller vertical facings so as not + // to hit the ground prematurely + if (targetPassedBy) + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn); + else + { // Before the target is passed by, missile speed should be changed + // Target's height above loop's center + var tarHgt = (loopRadius * WAngle.FromFacing(vFacing).Cos() / 1024 - System.Math.Abs(relTarHgt)).Clamp(0, loopRadius); + + // Target's horizontal distance from loop's center + var tarDist = Exts.ISqrt(loopRadius * loopRadius - tarHgt * tarHgt); + + // Missile's horizontal distance from loop's center + var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + + // If the current height does not permit the missile + // to hit the target before passing it by, lower speed + // Otherwise, increase speed + if (relTarHorDist <= tarDist - System.Math.Sign(relTarHgt) * missDist) + ChangeSpeed(-1); + else + ChangeSpeed(); + } + } + else + { + // Aim to attain cruise altitude as soon as possible while having the absolute value + // of vertical facing bound by the maximum vertical rate of turn + var vDist = new WVec(-diffClfMslHgt - info.CruiseAltitude.Length, -speed, 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn); + + ChangeSpeed(); + } + + return desiredVFacing; + } + + WVec HomingTick(World world, WVec tarDistVec, int relTarHorDist) + { + int predClfHgt; + int predClfDist; + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist); + + // Height difference between the incline height and missile height + var diffClfMslHgt = predClfHgt - pos.Z; + + // Get underestimate of distance from target in next tick + var nxtRelTarHorDist = (relTarHorDist - speed - info.Acceleration.Length).Clamp(0, relTarHorDist); + + // Target height relative to the missile + var relTarHgt = tarDistVec.Z; + + // Compute which direction the projectile should be facing + var desiredHFacing = OpenRA.Traits.Util.GetFacing(tarDistVec + predVel, hFacing); + var desiredVFacing = HomingInnerTick(predClfDist, diffClfMslHgt, relTarHorDist, nxtRelTarHorDist, relTarHgt, vFacing, targetPassedBy); + + // The target will be passed by at the end of the tick + if (nxtRelTarHorDist == 0) + targetPassedBy = true; + + // 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 + return 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; + } + public void Tick(World world) { ticks++; if (anim != null) anim.Tick(); - 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); + state = States.Homing; speed = velocity.Length; + + // Compute the vertical loop radius + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn); } // Switch from homing mode to freefall mode if (info.RangeLimit != 0 && ticks == info.RangeLimit + 1) { - activated = false; + state = States.Freefall; 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) + var newTarPos = targetPosition; if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn) - targetPosition = args.GuidedTarget.CenterPosition + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); + newTarPos = args.GuidedTarget.CenterPosition + + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); + + // Compute target's predicted velocity vector (assuming uniform circular motion) + var fac1 = OpenRA.Traits.Util.GetFacing(tarVel, hFacing); + tarVel = newTarPos - targetPosition; + var fac2 = OpenRA.Traits.Util.GetFacing(tarVel, hFacing); + predVel = tarVel.Rotate(WRot.FromFacing(fac2 - fac1)); + targetPosition = newTarPos; // Compute current distance from target position - var dist = targetPosition + offset - pos; - var len = dist.Length; - var hLenCurr = dist.HorizontalLength; + var tarDistVec = targetPosition + offset - pos; + var relTarDist = tarDistVec.Length; + var relTarHorDist = tarDistVec.HorizontalLength; WVec move; - if (activated) - { - // 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; - } + if (state == States.Freefall) + move = FreefallTick(); else - { - // 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; - } + move = HomingTick(world, tarDistVec, relTarHorDist); - // 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) - { - 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; + renderFacing = WAngle.ArcTan(move.Z - move.Y, move.X).Angle / 4 - 64; + + // Move the missile + pos += move; // Create the smoke trail effect - if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (activated || info.TrailWhenDeactivated)) + if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (state != States.Freefall || info.TrailWhenDeactivated)) { world.AddFrameEndTask(w => w.Add(new Smoke(w, pos - 3 * move / 2, info.TrailImage, trailPalette, info.TrailSequence))); ticksToNextSmoke = info.TrailInterval; @@ -342,14 +656,18 @@ namespace OpenRA.Mods.Common.Effects if (info.ContrailLength > 0) contrail.Update(pos); + var cell = world.Map.CellContaining(pos); + + // NOTE: High speeds might cause the missile to miss the target or fly through obstacles + // In that case, big moves should probably be decomposed into multiple smaller ones with hit checks var height = world.Map.DistanceAboveTerrain(pos); var shouldExplode = (height.Length <= 0) // Hit the ground - || (len < info.CloseEnough.Length) // Within range + || (relTarDist < 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 - || (height.Length < info.AirburstAltitude.Length && hLenCurr < info.CloseEnough.Length); // Airburst + || (height.Length < info.AirburstAltitude.Length && relTarHorDist < 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 14856bcfaa..5e6e53879d 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -2718,10 +2718,10 @@ namespace OpenRA.Mods.Common.UtilityCommands } } - if (engineVersion < 20150912) + if (engineVersion < 20151009) { if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "Speed") - node.Key = "InitialSpeed"; + node.Key = "MaximumLaunchSpeed"; if (depth == 2 && parentKey == "Projectile" && parent.Value.Value == "Missile" && node.Key == "RateOfTurn") node.Key = "HorizontalRateOfTurn"; From 12b4f8ccf49bec2027b6ccbe121deea4a2cd4c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Husti=C4=87?= Date: Fri, 9 Oct 2015 00:24:34 +0200 Subject: [PATCH 3/4] Programatic YAML upgrade --- mods/cnc/weapons/missiles.yaml | 60 ++++++++++++++--------------- mods/d2k/weapons.yaml | 28 +++++++------- mods/ra/maps/fort-lonestar/map.yaml | 4 +- mods/ra/weapons/missiles.yaml | 50 ++++++++++++------------ mods/ts/weapons/energyweapons.yaml | 4 +- mods/ts/weapons/missiles.yaml | 42 ++++++++++---------- mods/ts/weapons/superweapons.yaml | 4 +- 7 files changed, 96 insertions(+), 96 deletions(-) diff --git a/mods/cnc/weapons/missiles.yaml b/mods/cnc/weapons/missiles.yaml index f6d97efbd4..eccf061eea 100644 --- a/mods/cnc/weapons/missiles.yaml +++ b/mods/cnc/weapons/missiles.yaml @@ -9,10 +9,10 @@ Rockets: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 15 - Trail: smokey + HorizontalRateOfTurn: 15 + TrailImage: smokey ContrailLength: 8 - Speed: 298 + MaximumLaunchSpeed: 298 RangeLimit: 20 Warhead@1Dam: SpreadDamage Spread: 128 @@ -43,10 +43,10 @@ BikeRockets: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 10 - Trail: smokey + HorizontalRateOfTurn: 10 + TrailImage: smokey ContrailLength: 8 - Speed: 213 + MaximumLaunchSpeed: 213 RangeLimit: 30 Warhead@1Dam: SpreadDamage Spread: 128 @@ -78,10 +78,10 @@ OrcaAGMissiles: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 20 - Trail: smokey + HorizontalRateOfTurn: 20 + TrailImage: smokey ContrailLength: 8 - Speed: 256 + MaximumLaunchSpeed: 256 RangeLimit: 30 Warhead@1Dam: SpreadDamage Spread: 128 @@ -112,10 +112,10 @@ OrcaAAMissiles: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 20 - Trail: smokey + HorizontalRateOfTurn: 20 + TrailImage: smokey ContrailLength: 8 - Speed: 298 + MaximumLaunchSpeed: 298 RangeLimit: 30 Warhead@1Dam: SpreadDamage Spread: 128 @@ -143,10 +143,10 @@ MammothMissiles: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 20 - Trail: smokey + HorizontalRateOfTurn: 20 + TrailImage: smokey ContrailLength: 8 - Speed: 341 + MaximumLaunchSpeed: 341 RangeLimit: 35 Warhead@1Dam: SpreadDamage Spread: 298 @@ -216,10 +216,10 @@ MammothMissiles: Blockable: false Inaccuracy: 213 Image: DRAGON - RateOfTurn: 10 - Trail: smokey + HorizontalRateOfTurn: 10 + TrailImage: smokey ContrailLength: 8 - Speed: 213 + MaximumLaunchSpeed: 213 RangeLimit: 40 Warhead@1Dam: SpreadDamage Spread: 128 @@ -248,10 +248,10 @@ BoatMissile: Blockable: false Inaccuracy: 213 Image: DRAGON - RateOfTurn: 10 - Trail: smokey + HorizontalRateOfTurn: 10 + TrailImage: smokey ContrailLength: 8 - Speed: 170 + MaximumLaunchSpeed: 170 RangeLimit: 60 Warhead@1Dam: SpreadDamage Spread: 256 @@ -283,10 +283,10 @@ TowerMissle: Blockable: false Inaccuracy: 128 Image: DRAGON - RateOfTurn: 20 - Trail: smokey + HorizontalRateOfTurn: 20 + TrailImage: smokey ContrailLength: 8 - Speed: 298 + MaximumLaunchSpeed: 298 RangeLimit: 40 Warhead@1Dam: SpreadDamage Spread: 683 @@ -313,10 +313,10 @@ SAMMissile: Arm: 0 Blockable: false Image: MISSILE - RateOfTurn: 20 - Speed: 426 + HorizontalRateOfTurn: 20 + MaximumLaunchSpeed: 426 RangeLimit: 35 - Trail: smokey + TrailImage: smokey ContrailLength: 8 Warhead@1Dam: SpreadDamage Spread: 682 @@ -372,10 +372,10 @@ Patriot: Projectile: Missile Blockable: false Image: patriot - Trail: smokey + TrailImage: smokey ContrailLength: 8 - RateOfTurn: 20 - Speed: 341 + HorizontalRateOfTurn: 20 + MaximumLaunchSpeed: 341 RangeLimit: 30 Warhead@1Dam: SpreadDamage Spread: 682 diff --git a/mods/d2k/weapons.yaml b/mods/d2k/weapons.yaml index 85e50dd447..d68c5ad5f8 100644 --- a/mods/d2k/weapons.yaml +++ b/mods/d2k/weapons.yaml @@ -29,11 +29,11 @@ Bazooka: Report: ROCKET1.WAV ValidTargets: Ground Projectile: Missile - Speed: 281 + MaximumLaunchSpeed: 281 Inaccuracy: 64 Image: RPG - RateOfTurn: 1 - Trail: bazooka_trail2 + HorizontalRateOfTurn: 1 + TrailImage: bazooka_trail2 TrailPalette: effect75alpha TrailInterval: 1 RangeLimit: 35 @@ -236,11 +236,11 @@ Rocket: Projectile: Missile Inaccuracy: 64 Image: RPG - RateOfTurn: 0 - Trail: bazooka_trail2 + HorizontalRateOfTurn: 0 + TrailImage: bazooka_trail2 TrailPalette: effect75alpha TrailInterval: 1 - Speed: 343 + MaximumLaunchSpeed: 343 RangeLimit: 40 Warhead@1Dam: SpreadDamage Spread: 250 @@ -309,12 +309,12 @@ TowerMissile: Projectile: Missile Blockable: false Shadow: true - RateOfTurn: 1 + HorizontalRateOfTurn: 1 Inaccuracy: 384 Image: MISSILE2 - Trail: large_trail + TrailImage: large_trail TrailInterval: 1 - Speed: 320 + MaximumLaunchSpeed: 320 Warhead@1Dam: SpreadDamage Spread: 280 Falloff: 100, 100, 100, 95, 60, 25, 0 @@ -463,13 +463,13 @@ mtank_pri: Report: ROCKET1.WAV ValidTargets: Ground, Air Projectile: Missile - Speed: 281 - RateOfTurn: 3 + MaximumLaunchSpeed: 281 + HorizontalRateOfTurn: 3 Blockable: false Shadow: yes Inaccuracy: 96 Image: MISSILE2 - Trail: large_trail + TrailImage: large_trail TrailInterval: 1 Warhead@1Dam: SpreadDamage Spread: 280 @@ -500,13 +500,13 @@ DeviatorMissile: Report: MISSLE1.WAV InvalidTargets: Infantry, Structure Projectile: Missile - Speed: 281 + MaximumLaunchSpeed: 281 Blockable: false Shadow: yes RateofTurn: 1 Inaccuracy: 256 Image: MISSILE - Trail: deviator_trail + TrailImage: deviator_trail TrailPalette: deviatorgas TrailUsePlayerPalette: true TrailInterval: 1 diff --git a/mods/ra/maps/fort-lonestar/map.yaml b/mods/ra/maps/fort-lonestar/map.yaml index c1966d92ac..5c5244cc8c 100644 --- a/mods/ra/maps/fort-lonestar/map.yaml +++ b/mods/ra/maps/fort-lonestar/map.yaml @@ -817,12 +817,12 @@ Weapons: Burst: 2 ValidTargets: Ground, Air Projectile: Missile - Speed: 128 + MaximumLaunchSpeed: 128 Arm: 2 High: true Shadow: false Proximity: true - Trail: smokey + TrailImage: smokey ContrailLength: 150 Inaccuracy: 853 Image: DRAGON diff --git a/mods/ra/weapons/missiles.yaml b/mods/ra/weapons/missiles.yaml index 3194d04db4..a23cbe777e 100644 --- a/mods/ra/weapons/missiles.yaml +++ b/mods/ra/weapons/missiles.yaml @@ -7,13 +7,13 @@ Maverick: BurstDelay: 7 ValidTargets: Ground, Water Projectile: Missile - Speed: 256 + MaximumLaunchSpeed: 256 Arm: 2 Blockable: false ContrailLength: 10 Inaccuracy: 512 Image: DRAGON - RateOfTurn: 5 + HorizontalRateOfTurn: 5 RangeLimit: 60 Warhead@1Dam: SpreadDamage Spread: 128 @@ -43,14 +43,14 @@ Dragon: Report: MISSILE6.AUD ValidTargets: Ground, Water Projectile: Missile - Speed: 213 + MaximumLaunchSpeed: 213 Arm: 2 Blockable: false - Trail: smokey + TrailImage: smokey ContrailLength: 10 Inaccuracy: 128 Image: DRAGON - RateOfTurn: 5 + HorizontalRateOfTurn: 5 RangeLimit: 35 Warhead@1Dam: SpreadDamage Spread: 128 @@ -82,13 +82,13 @@ HellfireAG: BurstDelay: 10 ValidTargets: Ground, Water Projectile: Missile - Speed: 256 + MaximumLaunchSpeed: 256 Arm: 2 Blockable: false ContrailLength: 10 Inaccuracy: 128 Image: DRAGON - RateOfTurn: 10 + HorizontalRateOfTurn: 10 RangeLimit: 20 Warhead@1Dam: SpreadDamage Spread: 128 @@ -120,13 +120,13 @@ HellfireAA: BurstDelay: 10 ValidTargets: Air Projectile: Missile - Speed: 384 + MaximumLaunchSpeed: 384 Arm: 2 Blockable: false ContrailLength: 10 Inaccuracy: 128 Image: DRAGON - RateOfTurn: 10 + HorizontalRateOfTurn: 10 RangeLimit: 20 Warhead@1Dam: SpreadDamage Spread: 128 @@ -156,13 +156,13 @@ MammothTusk: Burst: 2 ValidTargets: Air, Infantry Projectile: Missile - Speed: 341 + MaximumLaunchSpeed: 341 Arm: 2 Blockable: false ContrailLength: 10 Inaccuracy: 128 Image: DRAGON - RateOfTurn: 15 + HorizontalRateOfTurn: 15 RangeLimit: 40 Warhead@1Dam: SpreadDamage Spread: 256 @@ -199,9 +199,9 @@ Nike: Blockable: false ContrailLength: 10 Image: MISSILE - RateOfTurn: 25 + HorizontalRateOfTurn: 25 RangeLimit: 50 - Speed: 341 + MaximumLaunchSpeed: 341 Warhead@1Dam: SpreadDamage Spread: 128 Damage: 50 @@ -227,9 +227,9 @@ RedEye: Blockable: false ContrailLength: 10 Image: MISSILE - RateOfTurn: 20 + HorizontalRateOfTurn: 20 RangeLimit: 30 - Speed: 298 + MaximumLaunchSpeed: 298 Warhead@1Dam: SpreadDamage Spread: 128 Damage: 40 @@ -291,9 +291,9 @@ Stinger: Blockable: false ContrailLength: 10 Image: DRAGON - RateOfTurn: 20 + HorizontalRateOfTurn: 20 RangeLimit: 50 - Speed: 170 + MaximumLaunchSpeed: 170 Warhead@1Dam: SpreadDamage Spread: 128 Damage: 30 @@ -331,9 +331,9 @@ StingerAA: Blockable: false ContrailLength: 10 Image: DRAGON - RateOfTurn: 20 + HorizontalRateOfTurn: 20 RangeLimit: 50 - Speed: 255 + MaximumLaunchSpeed: 255 Warhead@1Dam: SpreadDamage Spread: 128 Damage: 30 @@ -369,9 +369,9 @@ TorpTube: Projectile: Missile Image: MISSILE Arm: 3 - Speed: 85 - Trail: bubbles - RateOfTurn: 1 + MaximumLaunchSpeed: 85 + TrailImage: bubbles + HorizontalRateOfTurn: 1 RangeLimit: 160 BoundToTerrainType: Water Palette: shadow @@ -441,14 +441,14 @@ APTusk: Report: MISSILE6.AUD ValidTargets: Ground, Water Projectile: Missile - Speed: 298 + MaximumLaunchSpeed: 298 Arm: 2 Blockable: false - Trail: smokey + TrailImage: smokey ContrailLength: 10 Inaccuracy: 128 Image: DRAGON - RateOfTurn: 10 + HorizontalRateOfTurn: 10 RangeLimit: 22 Warhead@1Dam: SpreadDamage Spread: 128 diff --git a/mods/ts/weapons/energyweapons.yaml b/mods/ts/weapons/energyweapons.yaml index 489a883005..5d64bc5966 100644 --- a/mods/ts/weapons/energyweapons.yaml +++ b/mods/ts/weapons/energyweapons.yaml @@ -61,9 +61,9 @@ CyCannon: Report: SCRIN5B.AUD ValidTargets: Ground Projectile: Missile - Speed: 192 + MaximumLaunchSpeed: 192 Blockable: false - RateOfTurn: 2 + HorizontalRateOfTurn: 2 Shadow: true Image: TORPEDO Warhead@1Dam: SpreadDamage diff --git a/mods/ts/weapons/missiles.yaml b/mods/ts/weapons/missiles.yaml index 93c05509fa..9853813d56 100644 --- a/mods/ts/weapons/missiles.yaml +++ b/mods/ts/weapons/missiles.yaml @@ -5,13 +5,13 @@ Bazooka: Report: RKETINF1.AUD ValidTargets: Ground, Air Projectile: Missile - Speed: 213 + MaximumLaunchSpeed: 213 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 8 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 8 RangeLimit: 50 Palette: ra Warhead@1Dam: SpreadDamage @@ -48,14 +48,14 @@ HoverMissile: Report: HOVRMIS1.AUD ValidTargets: Ground, Air Projectile: Missile - Speed: 213 + MaximumLaunchSpeed: 213 Arm: 2 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 8 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 8 RangeLimit: 35 Palette: ra Warhead@1Dam: SpreadDamage @@ -96,9 +96,9 @@ MammothTusk: Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 10 - Speed: 213 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 10 + MaximumLaunchSpeed: 213 RangeLimit: 50 Palette: ra Warhead@1Dam: SpreadDamage @@ -135,9 +135,9 @@ BikeMissile: Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 8 - Speed: 213 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 8 + MaximumLaunchSpeed: 213 RangeLimit: 50 Palette: ra Warhead@1Dam: SpreadDamage @@ -170,14 +170,14 @@ Dragon: Report: MISL1.AUD ValidTargets: Ground, Air Projectile: Missile - Speed: 171 + MaximumLaunchSpeed: 171 Arm: 2 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 8 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 8 RangeLimit: 50 Palette: ra Warhead@1Dam: SpreadDamage @@ -214,14 +214,14 @@ Hellfire: Burst: 2 ValidTargets: Ground Projectile: Missile - Speed: 256 + MaximumLaunchSpeed: 256 Arm: 2 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 8 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 8 RangeLimit: 35 Palette: ra Warhead@1Dam: SpreadDamage @@ -257,14 +257,14 @@ RedEye2: Report: SAMSHOT1.AUD ValidTargets: Air Projectile: Missile - Speed: 384 + MaximumLaunchSpeed: 384 Arm: 2 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - Trail: small_smoke_trail - RateOfTurn: 5 + TrailImage: small_smoke_trail + HorizontalRateOfTurn: 5 RangeLimit: 100 Palette: ra Warhead@1Dam: SpreadDamage diff --git a/mods/ts/weapons/superweapons.yaml b/mods/ts/weapons/superweapons.yaml index 24e75d7bd6..2d3143a87f 100644 --- a/mods/ts/weapons/superweapons.yaml +++ b/mods/ts/weapons/superweapons.yaml @@ -4,13 +4,13 @@ MultiCluster: Report: MISL1.AUD ValidTargets: Ground Projectile: Missile - Speed: 170 + MaximumLaunchSpeed: 170 Arm: 2 Blockable: false Shadow: true Inaccuracy: 128 Image: DRAGON - RateOfTurn: 8 + HorizontalRateOfTurn: 8 RangeLimit: 35 Palette: ra Warhead@1Dam: SpreadDamage From cd8a15271cffa7275380a47ee87babcb9c0fcd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Husti=C4=87?= Date: Sun, 11 Oct 2015 19:18:43 +0200 Subject: [PATCH 4/4] Downward lookahead. Added downward lookahead capability. --- OpenRA.Mods.Common/Effects/Missile.cs | 417 +++++++++++++++++--------- 1 file changed, 272 insertions(+), 145 deletions(-) diff --git a/OpenRA.Mods.Common/Effects/Missile.cs b/OpenRA.Mods.Common/Effects/Missile.cs index 97f0a18f06..743989add0 100644 --- a/OpenRA.Mods.Common/Effects/Missile.cs +++ b/OpenRA.Mods.Common/Effects/Missile.cs @@ -35,19 +35,19 @@ namespace OpenRA.Mods.Common.Effects public readonly bool Shadow = false; [Desc("Minimum vertical launch angle (pitch).")] - public readonly WAngle MinimumLaunchAngle = new WAngle(-128); + public readonly WAngle MinimumLaunchAngle = WAngle.Zero; [Desc("Maximum vertical launch angle (pitch).")] public readonly WAngle MaximumLaunchAngle = new WAngle(64); [Desc("Minimum launch speed in WDist / tick")] - public readonly WDist MinimumLaunchSpeed = WDist.Zero; + public readonly WDist MinimumLaunchSpeed = new WDist(75); [Desc("Maximum launch speed in WDist / tick")] - public readonly WDist MaximumLaunchSpeed = new WDist(8); + public readonly WDist MaximumLaunchSpeed = new WDist(200); [Desc("Maximum projectile speed in WDist / tick")] - public readonly WDist MaximumSpeed = new WDist(512); + public readonly WDist MaximumSpeed = new WDist(384); [Desc("Projectile acceleration when propulsion activated.")] public readonly WDist Acceleration = new WDist(5); @@ -128,6 +128,7 @@ namespace OpenRA.Mods.Common.Effects public IEffect Create(ProjectileArgs args) { return new Missile(this, args); } } + // TODO: double check square roots!!! public class Missile : IEffect, ISync { enum States @@ -153,6 +154,7 @@ namespace OpenRA.Mods.Common.Effects States state; bool targetPassedBy; bool lockOn = false; + bool allowPassBy; // TODO: use this also with high minimum launch angle settings WPos targetPosition; WVec offset; @@ -191,7 +193,7 @@ namespace OpenRA.Mods.Common.Effects DetermineLaunchSpeedAndAngle(world, out speed, out vFacing); - velocity = new WVec(0, -info.MaximumLaunchSpeed.Length, 0) + 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))); @@ -218,67 +220,76 @@ namespace OpenRA.Mods.Common.Effects static int LoopRadius(int speed, int rot) { // loopRadius in w-units = speed in w-units per tick / angular speed in radians per tick - // angular speed in radians per tick = VROT in facing units per tick * (pi radians / 128 facing units) + // angular speed in radians per tick = rot in facing units per tick * (pi radians / 128 facing units) // pi = 314 / 100 - // ==> loopRadius = (speed * 128 * 100) / (314 * VROT) + // ==> loopRadius = (speed * 128 * 100) / (314 * rot) return (speed * 6400) / (157 * rot); } + void DetermineLaunchSpeedAndAngleForIncline(int predClfDist, int diffClfMslHgt, int relTarHorDist, + out int speed, out int vFacing) + { + speed = info.MaximumLaunchSpeed.Length; + + // Find smallest vertical facing, for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent with vertical facing 0 + vFacing = info.MaximumLaunchAngle.Angle >> 2; + + // Compute minimum speed necessary to both be able to face directly upwards and have enough space + // to hit the target without passing it by (and thus having to do horizontal loops) + var minSpeed = ((System.Math.Min(predClfDist * 1024 / (1024 - WAngle.FromFacing(vFacing).Sin()), + (relTarHorDist + predClfDist) * 1024 / (2 * (2048 - WAngle.FromFacing(vFacing).Sin()))) + * info.VerticalRateOfTurn * 157) / 6400).Clamp(info.MinimumLaunchSpeed.Length, info.MaximumLaunchSpeed.Length); + + if ((sbyte)vFacing < 0) + speed = minSpeed; + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt) + && !WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) + { + // Find highest speed greater than the above minimum that allows the missile + // to surmount the incline + var vFac = vFacing; + speed = BisectionSearch(minSpeed, info.MaximumLaunchSpeed.Length, spd => + { + var lpRds = LoopRadius(spd, info.VerticalRateOfTurn); + return WillClimbWithinDistance(vFac, lpRds, predClfDist, diffClfMslHgt) + || WillClimbAroundInclineTop(vFac, lpRds, predClfDist, diffClfMslHgt, spd); + }); + } + else + { + // Find least vertical facing that will allow the missile to climb + // terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + vFacing = BisectionSearch(System.Math.Max((sbyte)(info.MinimumLaunchAngle.Angle >> 2), (sbyte)0), + (sbyte)(info.MaximumLaunchAngle.Angle >> 2), + vFac => !WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)) + 1; + } + } + + // TODO: Double check Launch parameter determination void DetermineLaunchSpeedAndAngle(World world, out int speed, out int vFacing) { speed = info.MaximumLaunchSpeed.Length; - var loopRadius = LoopRadius(speed, info.VerticalRateOfTurn); + loopRadius = LoopRadius(speed, info.VerticalRateOfTurn); // Compute current distance from target position var tarDistVec = targetPosition + offset - pos; var relTarHorDist = tarDistVec.HorizontalLength; - int predClfHgt; - int predClfDist; - InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist); + int predClfHgt, predClfDist, lastHtChg, lastHt; + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out lastHtChg, out lastHt); // Height difference between the incline height and missile height var diffClfMslHgt = predClfHgt - pos.Z; // Incline coming up if (diffClfMslHgt >= 0) + DetermineLaunchSpeedAndAngleForIncline(predClfDist, diffClfMslHgt, relTarHorDist, out speed, out vFacing); + else if (lastHt != 0) { - // Find smallest vertical facing, attainable in the next tick, - // for which the missile will be able to climb terrAltDiff w-units - // within hHeightChange w-units all the while ending the ascent - // with vertical facing 0 - vFacing = info.MaximumLaunchAngle.Angle >> 2; - - // Compute minimum speed necessary to both be able to face directly upwards and - // have enough space to hit the target without passing it by (and thus having to - // do horizontal loops) - var minSpeed = ((System.Math.Min(predClfDist * 1024 / (1024 - WAngle.FromFacing(vFacing).Sin()), - (relTarHorDist + predClfDist) * 1024 / (2 * (2048 - WAngle.FromFacing(vFacing).Sin()))) - * info.VerticalRateOfTurn * 157) / 6400).Clamp(info.MinimumLaunchSpeed.Length, info.MaximumLaunchSpeed.Length); - - if ((sbyte)vFacing < 0) - speed = minSpeed; - else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt) - && !WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) - { - // Find highest speed greater than the above minimum that allows the missile - // to surmount the incline - var vFac = vFacing; - speed = BisectionSearch(minSpeed, info.MaximumLaunchSpeed.Length, spd => - { - var lpRds = LoopRadius(spd, info.VerticalRateOfTurn); - return WillClimbWithinDistance(vFac, lpRds, predClfDist, diffClfMslHgt) - || WillClimbAroundInclineTop(vFac, lpRds, predClfDist, diffClfMslHgt, spd); - }); - } - else - { - // Find least vertical facing that will allow the missile to climb - // terrAltDiff w-units within hHeightChange w-units - // all the while ending the ascent with vertical facing 0 - vFacing = BisectionSearch(0, info.MaximumLaunchAngle.Angle, - vFac => !WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)) + 1; - } + vFacing = System.Math.Max((sbyte)(info.MinimumLaunchAngle.Angle >> 2), (sbyte)0); + speed = info.MaximumLaunchSpeed.Length; } else { @@ -299,13 +310,13 @@ namespace OpenRA.Mods.Common.Effects // Will missile be able to climb terrAltDiff w-units within hHeightChange w-units // all the while ending the ascent with vertical facing 0 - // Calling this function only makes sense when vFacing is nonzero + // Calling this function only makes sense when vFacing is nonnegative static bool WillClimbWithinDistance(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt) { // Missile's horizontal distance from loop's center var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; - // Missile's height above loop's center + // Missile's height below loop's top var missHgt = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024; // Height that would be climbed without changing vertical facing @@ -392,13 +403,15 @@ namespace OpenRA.Mods.Common.Effects // NOTE: It might be desirable to make lookahead more intelligent by outputting more information // than just the highest point in the lookahead distance - void InclineLookahead(World world, int distCheck, out int predClfHgt, out int predClfDist) + void InclineLookahead(World world, int distCheck, out int predClfHgt, out int predClfDist, out int lastHtChg, out int lastHt) { predClfHgt = 0; // Highest probed terrain height predClfDist = 0; // Distance from highest point + lastHtChg = 0; // Distance from last time the height changes + lastHt = 0; // Height just before the last height change // NOTE: Might be desired to unhardcode the lookahead step size - var stepSize = 128; + var stepSize = 32; var step = new WVec(0, -stepSize, 0) .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); // Step vector of length 128 @@ -408,30 +421,94 @@ namespace OpenRA.Mods.Common.Effects var posProbe = pos; var curDist = 0; var tickLimit = System.Math.Min(maxLookaheadDistance, distCheck) / stepSize; + var prevHt = 0; + + // TODO: Make sure cell on map!!! for (var tick = 0; tick <= tickLimit; tick++) { posProbe += step; - curDist += stepSize; + if (!world.Map.Contains(world.Map.CellContaining(posProbe))) + break; + var ht = world.Map.MapHeight.Value[world.Map.CellContaining(posProbe)] * 512; + + curDist += stepSize; if (ht > predClfHgt) { predClfHgt = ht; predClfDist = curDist; } + + if (prevHt != ht) + { + lastHtChg = curDist; + lastHt = prevHt; + prevHt = ht; + } } } - int HomingInnerTick(int predClfDist, int diffClfMslHgt, int relTarHorDist, + int IncreaseAltitude(int predClfDist, int diffClfMslHgt, int relTarHorDist, int vFacing) + { + var desiredVFacing = vFacing; + + // If missile is below incline top height and facing downwards, bring back + // its vertical facing above zero as soon as possible + if ((sbyte)vFacing < 0) + desiredVFacing = info.VerticalRateOfTurn; + + // Missile will climb around incline top if bringing vertical facing + // down to zero on an arc of radius loopRadius + else if (IsNearInclineTop(vFacing, loopRadius, predClfDist) + && WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) + desiredVFacing = 0; + + // Missile will not climb terrAltDiff w-units within hHeightChange w-units + // all the while ending the ascent with vertical facing 0 + else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt)) + + // Find smallest vertical facing, attainable in the next tick, + // for which the missile will be able to climb terrAltDiff w-units + // within hHeightChange w-units all the while ending the ascent + // with vertical facing 0 + for (var vFac = System.Math.Min(vFacing + info.VerticalRateOfTurn - 1, 63); vFac >= vFacing; vFac--) + if (!WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt) + && !(predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFac).Sin()) / 1024 + && WillClimbAroundInclineTop(vFac, loopRadius, predClfDist, diffClfMslHgt, speed))) + { + desiredVFacing = vFac + 1; + break; + } + + // Attained height after ascent as predicted from upper part of incline surmounting manoeuvre + var predAttHght = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024 - diffClfMslHgt; + + // Should the missile be slowed down in order to make it more manoeuverable + var slowDown = info.Acceleration.Length != 0 // Possible to decelerate + && ((desiredVFacing != 0 // Lower part of incline surmounting manoeuvre + + // Incline will be hit before vertical facing attains 64 + && (predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 + + // When evaluating this the incline will be *not* be hit before vertical facing attains 64 + // At current speed target too close to hit without passing it by + || relTarHorDist <= 2 * loopRadius * (2048 - WAngle.FromFacing(vFacing).Sin()) / 1024 - predClfDist)) + + || (desiredVFacing == 0 // Upper part of incline surmounting manoeuvre + && relTarHorDist <= loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024 + + Exts.ISqrt(predAttHght * (2 * loopRadius - predAttHght)))); // Target too close to hit at current speed + + if (slowDown) + ChangeSpeed(-1); + + return desiredVFacing; + } + + int HomingInnerTick(int predClfDist, int diffClfMslHgt, int relTarHorDist, int lastHtChg, int lastHt, int nxtRelTarHorDist, int relTarHgt, int vFacing, bool targetPassedBy) { int desiredVFacing = vFacing; - // NOTE: Might be desired to unhardcode the distance from target - // at which the missile no longer cruises at cruise altitude - // but instead keeps trying to hit the target - // It still avoids inclines however - int targetLockonDistance = 2 * loopRadius; - // Incline coming up -> attempt to reach the incline so that after predClfDist // the height above the terrain is positive but as close to 0 as possible // Also, never change horizontal facing and never travel backwards @@ -442,96 +519,137 @@ namespace OpenRA.Mods.Common.Effects // the missile hasn't been fired near a cliff) is simply finding the smallest // vertical facing that allows for a smooth climb to the new terrain's height // and coming in at predClfDist at exactly zero vertical facing - if (diffClfMslHgt >= 0) - { - // If missile is below incline top height and facing downwards, bring back - // its vertical facing above zero as soon as possible - if ((sbyte)vFacing < 0) - desiredVFacing = info.VerticalRateOfTurn; - - // Missile will climb around incline top if bringing vertical facing - // down to zero on an arc of radius loopRadius - else if (IsNearInclineTop(vFacing, loopRadius, predClfDist) - && WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt, speed)) - desiredVFacing = 0; - - // Missile will not climb terrAltDiff w-units within hHeightChange w-units - // all the while ending the ascent with vertical facing 0 - else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt)) - - // Find smallest vertical facing, attainable in the next tick, - // for which the missile will be able to climb terrAltDiff w-units - // within hHeightChange w-units all the while ending the ascent - // with vertical facing 0 - for (var vFac = System.Math.Min(vFacing + info.VerticalRateOfTurn - 1, 63); vFac > vFacing; vFac--) - if (!WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt) - && !(predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFac).Sin()) / 1024 - && WillClimbAroundInclineTop(vFac, loopRadius, predClfDist, diffClfMslHgt, speed))) - { - desiredVFacing = vFac + 1; - break; - } - - // Attained height after ascent as predicted from upper part of incline surmounting manoeuvre - var predAttHght = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024 - diffClfMslHgt; - - // Should the missile be slowed down in order to make it more manoeuverable - var slowDown = info.Acceleration.Length != 0 // Possible to decelerate - && ((desiredVFacing != 0 // Lower part of incline surmounting manoeuvre - - // Incline will be hit before vertical facing attains 64 - && (predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024 - - // When evaluating this the incline will be *not* be hit before vertical facing attains 64 - // At current speed target too close to hit without passing it by - || relTarHorDist <= 2 * loopRadius * (2048 - WAngle.FromFacing(vFacing).Sin()) / 1024 - predClfDist)) - - || (desiredVFacing == 0 // Upper part of incline surmounting manoeuvre - && relTarHorDist <= loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024 - + Exts.ISqrt(predAttHght * (2 * loopRadius - predAttHght)))); // Target too close to hit at current speed - - if (slowDown) - ChangeSpeed(-1); - } - else if (nxtRelTarHorDist <= 2 * loopRadius || state == States.Hitting) + if (diffClfMslHgt >= 0 && !allowPassBy) + desiredVFacing = IncreaseAltitude(predClfDist, diffClfMslHgt, relTarHorDist, vFacing); + else if (relTarHorDist <= 3 * loopRadius || state == States.Hitting) { // No longer travel at cruise altitude state = States.Hitting; - // Aim for the target - var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); - desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + if (lastHt >= targetPosition.Z) + allowPassBy = true; - // Do not accept -1 as valid vertical facing since it is usually a numerical error - // and will lead to premature descent and crashing into the ground - if (desiredVFacing == -1) - desiredVFacing = 0; + if (!allowPassBy && (lastHt < targetPosition.Z || targetPassedBy)) + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist, 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); - // If the target has been passed by, limit the absolute value of - // vertical facing by the maximum vertical rate of turn - // Do this because the missile will be looping horizontally - // and thus needs smaller vertical facings so as not - // to hit the ground prematurely - if (targetPassedBy) - desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn); - else - { // Before the target is passed by, missile speed should be changed - // Target's height above loop's center - var tarHgt = (loopRadius * WAngle.FromFacing(vFacing).Cos() / 1024 - System.Math.Abs(relTarHgt)).Clamp(0, loopRadius); + // Do not accept -1 as valid vertical facing since it is usually a numerical error + // and will lead to premature descent and crashing into the ground + if (desiredVFacing == -1) + desiredVFacing = 0; - // Target's horizontal distance from loop's center - var tarDist = Exts.ISqrt(loopRadius * loopRadius - tarHgt * tarHgt); + // If the target has been passed by, limit the absolute value of + // vertical facing by the maximum vertical rate of turn + // Do this because the missile will be looping horizontally + // and thus needs smaller vertical facings so as not + // to hit the ground prematurely + if (targetPassedBy) + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn); + else if (lastHt == 0) + { // Before the target is passed by, missile speed should be changed + // Target's height above loop's center + var tarHgt = (loopRadius * WAngle.FromFacing(vFacing).Cos() / 1024 - System.Math.Abs(relTarHgt)).Clamp(0, loopRadius); - // Missile's horizontal distance from loop's center - var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + // Target's horizontal distance from loop's center + var tarDist = Exts.ISqrt(loopRadius * loopRadius - tarHgt * tarHgt); - // If the current height does not permit the missile - // to hit the target before passing it by, lower speed - // Otherwise, increase speed - if (relTarHorDist <= tarDist - System.Math.Sign(relTarHgt) * missDist) - ChangeSpeed(-1); + // Missile's horizontal distance from loop's center + var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024; + + // If the current height does not permit the missile + // to hit the target before passing it by, lower speed + // Otherwise, increase speed + if (relTarHorDist <= tarDist - System.Math.Sign(relTarHgt) * missDist) + ChangeSpeed(-1); + else + ChangeSpeed(); + } + } + else if (allowPassBy || (lastHt != 0 && relTarHorDist - lastHtChg < loopRadius)) + { + // Only activate this part if target too close to cliff + allowPassBy = true; + + // Vector from missile's current position pointing to the loop's center + var radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFacing))); + + // Vector from loop's center to incline top hardcoded in height buffer zone + var edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius; + + if (!targetPassedBy) + { + // Climb to critical height + if (relTarHorDist > 2 * loopRadius) + { + // Target's distance from cliff + var d1 = relTarHorDist - lastHtChg; + if (d1 < 0) + d1 = 0; + if (d1 > 2 * loopRadius) + return 0; + + // Find critical height at which the missile must be once it is at one loopRadius + // away from the target + var h1 = loopRadius - Exts.ISqrt(d1 * (2 * loopRadius - d1)) - (pos.Z - lastHt); + + if (h1 > loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024) + desiredVFacing = WAngle.ArcTan(Exts.ISqrt(h1 * (2 * loopRadius - h1)), loopRadius - h1).Angle >> 2; + else + desiredVFacing = 0; + + // TODO: deceleration checks!!! + } + else + { + // Avoid the cliff edge + if (edgeVector.Length > loopRadius && lastHt > targetPosition.Z) + { + int vFac; + for (vFac = vFacing + 1; vFac <= vFacing + info.VerticalRateOfTurn - 1; vFac++) + { + // Vector from missile's current position pointing to the loop's center + radius = new WVec(loopRadius, 0, 0) + .Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFac))); + + // Vector from loop's center to incline top + 64 hardcoded in height buffer zone + edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius; + if (edgeVector.Length <= loopRadius) + break; + } + + desiredVFacing = vFac; + } + else + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist * (targetPassedBy ? -1 : 1), 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + if (desiredVFacing < 0 && info.VerticalRateOfTurn < (sbyte)vFacing) + desiredVFacing = 0; + } + } + } else - ChangeSpeed(); + { + // Aim for the target + var vDist = new WVec(-relTarHgt, -relTarHorDist * (targetPassedBy ? -1 : 1), 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + if (desiredVFacing < 0 && info.VerticalRateOfTurn < (sbyte)vFacing) + desiredVFacing = 0; + } + } + else + { + // Aim to attain cruise altitude as soon as possible while having the absolute value + // of vertical facing bound by the maximum vertical rate of turn + var vDist = new WVec(-diffClfMslHgt - info.CruiseAltitude.Length, -speed, 0); + desiredVFacing = (sbyte)OpenRA.Traits.Util.GetFacing(vDist, vFacing); + desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn); + + ChangeSpeed(); } } else @@ -550,9 +668,8 @@ namespace OpenRA.Mods.Common.Effects WVec HomingTick(World world, WVec tarDistVec, int relTarHorDist) { - int predClfHgt; - int predClfDist; - InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist); + int predClfHgt, predClfDist, lastHtChg, lastHt; + InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out lastHtChg, out lastHt); // Height difference between the incline height and missile height var diffClfMslHgt = predClfHgt - pos.Z; @@ -565,10 +682,20 @@ namespace OpenRA.Mods.Common.Effects // Compute which direction the projectile should be facing var desiredHFacing = OpenRA.Traits.Util.GetFacing(tarDistVec + predVel, hFacing); - var desiredVFacing = HomingInnerTick(predClfDist, diffClfMslHgt, relTarHorDist, nxtRelTarHorDist, relTarHgt, vFacing, targetPassedBy); - // The target will be passed by at the end of the tick - if (nxtRelTarHorDist == 0) + if (allowPassBy && System.Math.Abs(desiredHFacing - hFacing) >= System.Math.Abs(desiredHFacing + 128 - hFacing)) + { + desiredHFacing += 128; + targetPassedBy = true; + } + else + targetPassedBy = false; + + var desiredVFacing = HomingInnerTick(predClfDist, diffClfMslHgt, relTarHorDist, lastHtChg, lastHt, + nxtRelTarHorDist, relTarHgt, vFacing, targetPassedBy); + + // The target has been passed by + if (tarDistVec.HorizontalLength < speed * WAngle.FromFacing(vFacing).Cos() / 1024) targetPassedBy = true; // Check whether the homing mechanism is jammed