Move projectiles to their own namespace and folder

While they are (currently) technically effects, this makes the actual purpose and wiki more clear.
This commit is contained in:
reaperrr
2016-05-28 14:42:33 +02:00
parent 629f17b430
commit fef4f3eb79
10 changed files with 19 additions and 15 deletions

View File

@@ -1,221 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Effects
{
public class AreaBeamInfo : IProjectileInfo
{
[Desc("Projectile speed in WDist / tick, two values indicate a randomly picked velocity per beam.")]
public readonly WDist[] Speed = { new WDist(128) };
[Desc("The maximum duration (in ticks) of each beam burst.")]
public readonly int Duration = 10;
[Desc("The number of ticks between the beam causing warhead impacts in its area of effect.")]
public readonly int DamageInterval = 3;
[Desc("The width of the beam.")]
public readonly WDist Width = new WDist(512);
[Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical;
[Desc("How far beyond the target the projectile keeps on travelling.")]
public readonly WDist BeyondTargetRange = new WDist(0);
[Desc("Damage modifier applied at each range step.")]
public readonly int[] Falloff = { 100, 100 };
[Desc("Ranges at which each Falloff step is defined.")]
public readonly WDist[] Range = { WDist.Zero, new WDist(int.MaxValue) };
[Desc("Maximum offset at the maximum range.")]
public readonly WDist Inaccuracy = WDist.Zero;
[Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")]
public readonly bool Blockable = false;
[Desc("Extra search radius beyond beam width. Required to ensure affecting actors with large health radius.")]
public readonly WDist TargetExtraSearchRadius = new WDist(1536);
[Desc("Should the beam be visuall rendered? False = Beam is invisible.")]
public readonly bool RenderBeam = true;
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
public readonly int ZOffset = 0;
[Desc("Color of the beam.")]
public readonly Color Color = Color.Red;
[Desc("Beam color is the player's color.")]
public readonly bool UsePlayerColor = false;
public IEffect Create(ProjectileArgs args)
{
var c = UsePlayerColor ? args.SourceActor.Owner.Color.RGB : Color;
return new AreaBeam(this, args, c);
}
}
public class AreaBeam : IEffect, ISync
{
readonly AreaBeamInfo info;
readonly ProjectileArgs args;
readonly AttackBase actorAttackBase;
readonly Color color;
readonly WDist speed;
[Sync] WPos headPos;
[Sync] WPos tailPos;
[Sync] WPos target;
int length;
int towardsTargetFacing;
int headTicks;
int tailTicks;
bool isHeadTravelling = true;
bool isTailTravelling;
bool IsBeamComplete { get { return !isHeadTravelling && headTicks >= length &&
!isTailTravelling && tailTicks >= length; } }
public AreaBeam(AreaBeamInfo info, ProjectileArgs args, Color color)
{
this.info = info;
this.args = args;
this.color = color;
actorAttackBase = args.SourceActor.Trait<AttackBase>();
var world = args.SourceActor.World;
if (info.Speed.Length > 1)
speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length));
else
speed = info.Speed[0];
// Both the head and tail start at the source actor, but initially only the head is travelling.
headPos = args.Source;
tailPos = headPos;
target = args.PassiveTarget;
if (info.Inaccuracy.Length > 0)
{
var inaccuracy = Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers);
var maxOffset = inaccuracy * (target - headPos).Length / args.Weapon.Range.Length;
target += WVec.FromPDF(world.SharedRandom, 2) * maxOffset / 1024;
}
towardsTargetFacing = (target - headPos).Yaw.Facing;
// Update the target position with the range we shoot beyond the target by
// I.e. we can deliberately overshoot, so aim for that position
var dir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(towardsTargetFacing));
target += dir * info.BeyondTargetRange.Length / 1024;
length = Math.Max((target - headPos).Length / speed.Length, 1);
}
public void Tick(World world)
{
if (++headTicks >= length)
{
headPos = target;
isHeadTravelling = false;
}
else if (isHeadTravelling)
headPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, headTicks, length);
if (tailTicks <= 0 && args.SourceActor.IsInWorld && !args.SourceActor.IsDead)
{
args.Source = args.CurrentSource();
tailPos = args.Source;
}
// Allow for 1 cell (1024) leniency to avoid edge case stuttering (start firing and immediately stop again).
var outOfWeaponRange = args.Weapon.Range.Length + 1024 < (args.PassiveTarget - args.Source).Length;
// While the head is travelling, the tail must start to follow Duration ticks later.
// Alternatively, also stop emitting the beam if source actor dies or is ordered to stop.
if ((headTicks >= info.Duration && !isTailTravelling) || args.SourceActor.IsDead ||
!actorAttackBase.IsAttacking || outOfWeaponRange)
isTailTravelling = true;
if (isTailTravelling)
{
if (++tailTicks >= length)
{
tailPos = target;
isTailTravelling = false;
}
else
tailPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, tailTicks, length);
}
// Check for blocking actors
WPos blockedPos;
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, tailPos, headPos,
info.Width, info.TargetExtraSearchRadius, out blockedPos))
{
headPos = blockedPos;
target = headPos;
length = Math.Min(headTicks, length);
}
// Damage is applied to intersected actors every DamageInterval ticks
if (headTicks % info.DamageInterval == 0)
{
var actors = world.FindActorsOnLine(tailPos, headPos, info.Width, info.TargetExtraSearchRadius);
foreach (var a in actors)
{
var adjustedModifiers = args.DamageModifiers.Append(GetFalloff((args.Source - a.CenterPosition).Length));
args.Weapon.Impact(Target.FromActor(a), args.SourceActor, adjustedModifiers);
}
}
if (IsBeamComplete)
world.AddFrameEndTask(w => w.Remove(this));
}
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (!IsBeamComplete && info.RenderBeam && !(wr.World.FogObscures(tailPos) && wr.World.FogObscures(headPos)))
{
var beamRender = new BeamRenderable(headPos, info.ZOffset, tailPos - headPos, info.Shape, info.Width, color);
return new[] { (IRenderable)beamRender };
}
return SpriteRenderable.None;
}
int GetFalloff(int distance)
{
var inner = info.Range[0].Length;
for (var i = 1; i < info.Range.Length; i++)
{
var outer = info.Range[i].Length;
if (outer > distance)
return int2.Lerp(info.Falloff[i - 1], info.Falloff[i], distance - inner, outer - inner);
inner = outer;
}
return 0;
}
}
}

View File

@@ -1,235 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Effects
{
public class BulletInfo : IProjectileInfo
{
[Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")]
public readonly WDist[] Speed = { new WDist(17) };
[Desc("Maximum offset at the maximum range.")]
public readonly WDist Inaccuracy = WDist.Zero;
[Desc("Image to display.")]
public readonly string Image = null;
[Desc("Loop these sequences of Image while this projectile is moving.")]
[SequenceReference("Image")] public readonly string[] Sequences = { "idle" };
[Desc("The palette used to draw this projectile.")]
[PaletteReference] public readonly string Palette = "effect";
[Desc("Does this projectile have a shadow?")]
public readonly bool Shadow = false;
[Desc("Palette to use for this projectile's shadow if Shadow is true.")]
[PaletteReference] public readonly string ShadowPalette = "shadow";
[Desc("Trail animation.")]
public readonly string Trail = null;
[Desc("Loop these sequences of Trail while this projectile is moving.")]
[SequenceReference("Trail")] public readonly string[] TrailSequences = { "idle" };
[Desc("Is this blocked by actors with BlocksProjectiles trait.")]
public readonly bool Blockable = true;
[Desc("Width of projectile (used for finding blocking actors).")]
public readonly WDist Width = new WDist(1);
[Desc("Extra search radius beyond path for blocking actors.")]
public readonly WDist TargetExtraSearchRadius = new WDist(1536);
[Desc("Arc in WAngles, two values indicate variable arc.")]
public readonly WAngle[] Angle = { WAngle.Zero };
[Desc("Interval in ticks between each spawned Trail animation.")]
public readonly int TrailInterval = 2;
[Desc("Delay in ticks until trail animation is spawned.")]
public readonly int TrailDelay = 1;
[PaletteReference("TrailUsePlayerPalette")] public readonly string TrailPalette = "effect";
public readonly bool TrailUsePlayerPalette = false;
public readonly int ContrailLength = 0;
public readonly int ContrailZOffset = 2047;
public readonly Color ContrailColor = Color.White;
public readonly bool ContrailUsePlayerColor = false;
public readonly int ContrailDelay = 1;
public readonly WDist ContrailWidth = new WDist(64);
public IEffect Create(ProjectileArgs args) { return new Bullet(this, args); }
}
public class Bullet : IEffect, ISync
{
readonly BulletInfo info;
readonly ProjectileArgs args;
readonly Animation anim;
[Sync] readonly WAngle angle;
[Sync] readonly WDist speed;
ContrailRenderable contrail;
string trailPalette;
[Sync] WPos pos, target;
[Sync] int length;
[Sync] int facing;
[Sync] int ticks, smokeTicks;
[Sync] public Actor SourceActor { get { return args.SourceActor; } }
public Bullet(BulletInfo info, ProjectileArgs args)
{
this.info = info;
this.args = args;
pos = args.Source;
var world = args.SourceActor.World;
if (info.Angle.Length > 1)
angle = new WAngle(world.SharedRandom.Next(info.Angle[0].Angle, info.Angle[1].Angle));
else
angle = info.Angle[0];
if (info.Speed.Length > 1)
speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length));
else
speed = info.Speed[0];
target = args.PassiveTarget;
if (info.Inaccuracy.Length > 0)
{
var inaccuracy = Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers);
var range = Util.ApplyPercentageModifiers(args.Weapon.Range.Length, args.RangeModifiers);
var maxOffset = inaccuracy * (target - pos).Length / range;
target += WVec.FromPDF(world.SharedRandom, 2) * maxOffset / 1024;
}
facing = (target - pos).Yaw.Facing;
length = Math.Max((target - pos).Length / speed.Length, 1);
if (!string.IsNullOrEmpty(info.Image))
{
anim = new Animation(world, info.Image, new Func<int>(GetEffectiveFacing));
anim.PlayRepeating(info.Sequences.Random(world.SharedRandom));
}
if (info.ContrailLength > 0)
{
var color = info.ContrailUsePlayerColor ? ContrailRenderable.ChooseColor(args.SourceActor) : info.ContrailColor;
contrail = new ContrailRenderable(world, color, info.ContrailWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
}
trailPalette = info.TrailPalette;
if (info.TrailUsePlayerPalette)
trailPalette += args.SourceActor.Owner.InternalName;
smokeTicks = info.TrailDelay;
}
int GetEffectiveFacing()
{
var at = (float)ticks / (length - 1);
var attitude = angle.Tan() * (1 - 2 * at) / (4 * 1024);
var u = (facing % 128) / 128f;
var scale = 512 * u * (1 - u);
return (int)(facing < 128
? facing - scale * attitude
: facing + scale * attitude);
}
public void Tick(World world)
{
if (anim != null)
anim.Tick();
var lastPos = pos;
pos = WPos.LerpQuadratic(args.Source, target, angle, ticks, length);
// Check for walls or other blocking obstacles
var shouldExplode = false;
WPos blockedPos;
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, lastPos, pos, info.Width,
info.TargetExtraSearchRadius, out blockedPos))
{
pos = blockedPos;
shouldExplode = true;
}
if (!string.IsNullOrEmpty(info.Trail) && --smokeTicks < 0)
{
var delayedPos = WPos.LerpQuadratic(args.Source, target, angle, ticks - info.TrailDelay, length);
world.AddFrameEndTask(w => w.Add(new SpriteEffect(delayedPos, w, info.Trail, info.TrailSequences.Random(world.SharedRandom),
trailPalette, false, false, GetEffectiveFacing())));
smokeTicks = info.TrailInterval;
}
if (info.ContrailLength > 0)
contrail.Update(pos);
// Flight length reached / exceeded
shouldExplode |= ticks++ >= length;
if (shouldExplode)
Explode(world);
}
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (info.ContrailLength > 0)
yield return contrail;
if (anim == null || ticks >= length)
yield break;
var world = args.SourceActor.World;
if (!world.FogObscures(pos))
{
if (info.Shadow)
{
var dat = world.Map.DistanceAboveTerrain(pos);
var shadowPos = pos - new WVec(0, 0, dat.Length);
foreach (var r in anim.Render(shadowPos, wr.Palette(info.ShadowPalette)))
yield return r;
}
var palette = wr.Palette(info.Palette);
foreach (var r in anim.Render(pos, palette))
yield return r;
}
}
void Explode(World world)
{
if (info.ContrailLength > 0)
world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail)));
world.AddFrameEndTask(w => w.Remove(this));
args.Weapon.Impact(Target.FromPos(pos), args.SourceActor, args.DamageModifiers);
}
}
}

View File

@@ -1,110 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Effects
{
public class GravityBombInfo : IProjectileInfo
{
public readonly string Image = null;
[Desc("Sequence to loop while falling.")]
[SequenceReference("Image")] public readonly string Sequence = "idle";
[Desc("Sequence to play when launched. Skipped if null or empty.")]
[SequenceReference("Image")] public readonly string OpenSequence = null;
[PaletteReference] public readonly string Palette = "effect";
public readonly bool Shadow = false;
[PaletteReference] public readonly string ShadowPalette = "shadow";
public readonly WDist Velocity = WDist.Zero;
[Desc("Value added to velocity every tick.")]
public readonly WDist Acceleration = new WDist(15);
public IEffect Create(ProjectileArgs args) { return new GravityBomb(this, args); }
}
public class GravityBomb : IEffect, ISync
{
readonly GravityBombInfo info;
readonly Animation anim;
readonly ProjectileArgs args;
[Sync] WVec velocity;
[Sync] WPos pos;
[Sync] WVec acceleration;
public GravityBomb(GravityBombInfo info, ProjectileArgs args)
{
this.info = info;
this.args = args;
pos = args.Source;
velocity = new WVec(WDist.Zero, WDist.Zero, -info.Velocity);
acceleration = new WVec(WDist.Zero, WDist.Zero, info.Acceleration);
if (!string.IsNullOrEmpty(info.Image))
{
anim = new Animation(args.SourceActor.World, info.Image);
if (!string.IsNullOrEmpty(info.OpenSequence))
anim.PlayThen(info.OpenSequence, () => anim.PlayRepeating(info.Sequence));
else
anim.PlayRepeating(info.Sequence);
}
}
public void Tick(World world)
{
velocity -= acceleration;
pos += velocity;
if (pos.Z <= args.PassiveTarget.Z)
{
pos += new WVec(0, 0, args.PassiveTarget.Z - pos.Z);
world.AddFrameEndTask(w => w.Remove(this));
args.Weapon.Impact(Target.FromPos(pos), args.SourceActor, args.DamageModifiers);
}
if (anim != null)
anim.Tick();
}
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (anim == null)
yield break;
var world = args.SourceActor.World;
if (!world.FogObscures(pos))
{
if (info.Shadow)
{
var dat = world.Map.DistanceAboveTerrain(pos);
var shadowPos = pos - new WVec(0, 0, dat.Length);
foreach (var r in anim.Render(shadowPos, wr.Palette(info.ShadowPalette)))
yield return r;
}
var palette = wr.Palette(info.Palette);
foreach (var r in anim.Render(pos, palette))
yield return r;
}
}
}
}

View File

@@ -1,117 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Drawing;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Effects
{
[Desc("Not a sprite, but an engine effect.")]
class LaserZapInfo : IProjectileInfo
{
[Desc("The width of the zap.")]
public readonly WDist Width = new WDist(86);
[Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical;
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
public readonly int ZOffset = 0;
public readonly int BeamDuration = 10;
public readonly bool UsePlayerColor = false;
[Desc("Laser color in (A,)R,G,B.")]
public readonly Color Color = Color.Red;
[Desc("Impact animation.")]
public readonly string HitAnim = null;
[Desc("Sequence of impact animation to use.")]
[SequenceReference("HitAnim")] public readonly string HitAnimSequence = "idle";
[PaletteReference] public readonly string HitAnimPalette = "effect";
public IEffect Create(ProjectileArgs args)
{
var c = UsePlayerColor ? args.SourceActor.Owner.Color.RGB : Color;
return new LaserZap(args, this, c);
}
}
class LaserZap : IEffect
{
readonly ProjectileArgs args;
readonly LaserZapInfo info;
readonly Animation hitanim;
int ticks = 0;
Color color;
bool doneDamage;
bool animationComplete;
WPos target;
public LaserZap(ProjectileArgs args, LaserZapInfo info, Color color)
{
this.args = args;
this.info = info;
this.color = color;
target = args.PassiveTarget;
if (!string.IsNullOrEmpty(info.HitAnim))
hitanim = new Animation(args.SourceActor.World, info.HitAnim);
}
public void Tick(World world)
{
// Beam tracks target
if (args.GuidedTarget.IsValidFor(args.SourceActor))
target = args.GuidedTarget.CenterPosition;
if (!doneDamage)
{
if (hitanim != null)
hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true);
args.Weapon.Impact(Target.FromPos(target), args.SourceActor, args.DamageModifiers);
doneDamage = true;
}
if (hitanim != null)
hitanim.Tick();
if (++ticks >= info.BeamDuration && animationComplete)
world.AddFrameEndTask(w => w.Remove(this));
}
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (wr.World.FogObscures(target) &&
wr.World.FogObscures(args.Source))
yield break;
if (ticks < info.BeamDuration)
{
var rc = Color.FromArgb((info.BeamDuration - ticks) * 255 / info.BeamDuration, color);
yield return new BeamRenderable(args.Source, info.ZOffset, target - args.Source, info.Shape, info.Width, rc);
}
if (hitanim != null)
foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette)))
yield return r;
}
}
}

View File

@@ -1,883 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2016 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using OpenRA.Effects;
using OpenRA.GameRules;
using OpenRA.Graphics;
using OpenRA.Mods.Common;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
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("Minimum vertical launch angle (pitch).")]
public readonly WAngle MinimumLaunchAngle = new WAngle(-64);
[Desc("Maximum vertical launch angle (pitch).")]
public readonly WAngle MaximumLaunchAngle = new WAngle(128);
[Desc("Minimum launch speed in WDist / tick. Defaults to Speed if -1.")]
public readonly WDist MinimumLaunchSpeed = new WDist(-1);
[Desc("Maximum launch speed in WDist / tick. Defaults to Speed if -1.")]
public readonly WDist MaximumLaunchSpeed = new WDist(-1);
[Desc("Maximum projectile speed in WDist / tick")]
public readonly WDist Speed = new WDist(384);
[Desc("Projectile acceleration when propulsion activated.")]
public readonly WDist Acceleration = new WDist(5);
[Desc("How many ticks before this missile is armed and can explode.")]
public readonly int Arm = 0;
[Desc("Is the missile blocked by actors with BlocksProjectiles: trait.")]
public readonly bool Blockable = true;
[Desc("Width of projectile (used for finding blocking actors).")]
public readonly WDist Width = new WDist(1);
[Desc("Extra search radius beyond path for blocking actors.")]
public readonly WDist TargetExtraSearchRadius = new WDist(1536);
[Desc("Maximum offset at the maximum range")]
public readonly WDist Inaccuracy = WDist.Zero;
[Desc("Probability of locking onto and following target.")]
public readonly int LockOnProbability = 100;
[Desc("Horizontal rate of turn.")]
public readonly int HorizontalRateOfTurn = 5;
[Desc("Vertical rate of turn.")]
public readonly int VerticalRateOfTurn = 6;
[Desc("Gravity applied while in free fall.")]
public readonly int Gravity = 10;
[Desc("Run out of fuel after covering this distance. Zero for defaulting to weapon range. Negative for unlimited fuel.")]
public readonly WDist RangeLimit = WDist.Zero;
[Desc("Explode when running out of fuel.")]
public readonly bool ExplodeWhenEmpty = true;
[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("TrailImage")] 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 int ContrailZOffset = 2047;
public readonly WDist ContrailWidth = new WDist(64);
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 = "";
[Desc("Explodes when inside this proximity radius to target.",
"Note: If this value is lower than the missile speed, this check might",
"not trigger fast enough, causing the missile to fly past the target.")]
public readonly WDist CloseEnough = new WDist(298);
public IEffect Create(ProjectileArgs args) { return new Missile(this, args); }
}
// TODO: double check square roots!!!
public class Missile : IEffect, ISync
{
enum States
{
Freefall,
Homing,
Hitting
}
readonly MissileInfo info;
readonly ProjectileArgs args;
readonly Animation anim;
readonly WVec gravity;
int ticks;
int ticksToNextSmoke;
ContrailRenderable contrail;
string trailPalette;
States state;
bool targetPassedBy;
bool lockOn = false;
bool allowPassBy; // TODO: use this also with high minimum launch angle settings
WPos targetPosition;
WVec offset;
WVec tarVel;
WVec predVel;
[Sync] WPos pos;
WVec velocity;
int speed;
int loopRadius;
WDist distanceCovered;
WDist rangeLimit;
int renderFacing;
[Sync] int hFacing;
[Sync] int vFacing;
public Actor SourceActor { get { return args.SourceActor; } }
public Target GuidedTarget { get { return args.GuidedTarget; } }
public Missile(MissileInfo info, ProjectileArgs args)
{
this.info = info;
this.args = args;
pos = args.Source;
hFacing = args.Facing;
gravity = new WVec(0, 0, -info.Gravity);
targetPosition = args.PassiveTarget;
rangeLimit = info.RangeLimit != WDist.Zero ? info.RangeLimit : args.Weapon.Range;
var world = args.SourceActor.World;
if (info.Inaccuracy.Length > 0)
{
var inaccuracy = 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, -speed, 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, () => renderFacing);
anim.PlayRepeating(info.Sequence);
}
if (info.ContrailLength > 0)
{
var color = info.ContrailUsePlayerColor ? ContrailRenderable.ChooseColor(args.SourceActor) : info.ContrailColor;
contrail = new ContrailRenderable(world, color, info.ContrailWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
}
trailPalette = info.TrailPalette;
if (info.TrailUsePlayerPalette)
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 = rot in facing units per tick * (pi radians / 128 facing units)
// pi = 314 / 100
// ==> 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)
{
var minLaunchSpeed = info.MinimumLaunchSpeed.Length > -1 ? info.MinimumLaunchSpeed.Length : info.Speed.Length;
var maxLaunchSpeed = info.MaximumLaunchSpeed.Length > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.Length;
speed = info.MaximumLaunchSpeed.Length > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.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(minLaunchSpeed, maxLaunchSpeed);
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, maxLaunchSpeed, 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 > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.Length;
loopRadius = LoopRadius(speed, info.VerticalRateOfTurn);
// Compute current distance from target position
var tarDistVec = targetPosition + offset - pos;
var relTarHorDist = tarDistVec.HorizontalLength;
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 && predClfDist > 0)
DetermineLaunchSpeedAndAngleForIncline(predClfDist, diffClfMslHgt, relTarHorDist, out speed, out vFacing);
else if (lastHt != 0)
{
vFacing = System.Math.Max((sbyte)(info.MinimumLaunchAngle.Angle >> 2), (sbyte)0);
speed = info.MaximumLaunchSpeed.Length > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.Length;
}
else
{
// Set vertical facing so that the missile faces its target
var vDist = new WVec(-tarDistVec.Z, -relTarHorDist, 0);
vFacing = (sbyte)vDist.Yaw.Facing;
// 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 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 below loop's top
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<int, bool> 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<JamsMissiles> tp)
{
if ((tp.Actor.CenterPosition - pos).HorizontalLengthSquared > tp.Trait.Range.LengthSquared)
return false;
if (tp.Actor.Owner.Stances[args.SourceActor.Owner] == Stance.Ally && !tp.Trait.AlliedMissiles)
return false;
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.Speed.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.Speed.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, 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 = 32;
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;
var prevHt = 0;
// TODO: Make sure cell on map!!!
for (var tick = 0; tick <= tickLimit; tick++)
{
posProbe += step;
if (!world.Map.Contains(world.Map.CellContaining(posProbe)))
break;
var ht = world.Map.Height[world.Map.CellContaining(posProbe)] * 512;
curDist += stepSize;
if (ht > predClfHgt)
{
predClfHgt = ht;
predClfDist = curDist;
}
if (prevHt != ht)
{
lastHtChg = curDist;
lastHt = prevHt;
prevHt = ht;
}
}
}
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;
// 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 hitting 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 && !allowPassBy)
desiredVFacing = IncreaseAltitude(predClfDist, diffClfMslHgt, relTarHorDist, vFacing);
else if (relTarHorDist <= 3 * loopRadius || state == States.Hitting)
{
// No longer travel at cruise altitude
state = States.Hitting;
if (lastHt >= targetPosition.Z)
allowPassBy = true;
if (!allowPassBy && (lastHt < targetPosition.Z || targetPassedBy))
{
// Aim for the target
var vDist = new WVec(-relTarHgt, -relTarHorDist, 0);
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : 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 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);
// 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 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, 0);
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
if (desiredVFacing < 0 && info.VerticalRateOfTurn < (sbyte)vFacing)
desiredVFacing = 0;
}
}
}
else
{
// Aim for the target
var vDist = new WVec(-relTarHgt, relTarHorDist, 0);
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : 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)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
// If the missile is launched above CruiseAltitude, it has to descend instead of climbing
if (-diffClfMslHgt > info.CruiseAltitude.Length)
desiredVFacing = -desiredVFacing;
desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn);
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)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
// If the missile is launched above CruiseAltitude, it has to descend instead of climbing
if (-diffClfMslHgt > info.CruiseAltitude.Length)
desiredVFacing = -desiredVFacing;
desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn, info.VerticalRateOfTurn);
ChangeSpeed();
}
return desiredVFacing;
}
WVec HomingTick(World world, WVec tarDistVec, int relTarHorDist)
{
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;
// 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 velVec = tarDistVec + predVel;
var desiredHFacing = velVec.HorizontalLengthSquared != 0 ? velVec.Yaw.Facing : hFacing;
var delta = Util.NormalizeFacing(hFacing - desiredHFacing);
if (allowPassBy && delta > 64 && delta < 192)
{
desiredHFacing = (desiredHFacing + 128) & 0xFF;
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
var jammed = info.Jammable && world.ActorsWithTrait<JamsMissiles>().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 = Util.TickFacing(hFacing, desiredHFacing, info.HorizontalRateOfTurn);
vFacing = 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();
// Switch from freefall mode to homing mode
if (ticks == info.HomingActivationDelay + 1)
{
state = States.Homing;
speed = velocity.Length;
// Compute the vertical loop radius
loopRadius = LoopRadius(speed, info.VerticalRateOfTurn);
}
// Switch from homing mode to freefall mode
if (rangeLimit >= WDist.Zero && distanceCovered > rangeLimit)
{
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)
newTarPos = args.GuidedTarget.CenterPosition
+ new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude);
// Compute target's predicted velocity vector (assuming uniform circular motion)
var yaw1 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing);
tarVel = newTarPos - targetPosition;
var yaw2 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing);
predVel = tarVel.Rotate(WRot.FromYaw(yaw2 - yaw1));
targetPosition = newTarPos;
// Compute current distance from target position
var tarDistVec = targetPosition + offset - pos;
var relTarDist = tarDistVec.Length;
var relTarHorDist = tarDistVec.HorizontalLength;
WVec move;
if (state == States.Freefall)
move = FreefallTick();
else
move = HomingTick(world, tarDistVec, relTarHorDist);
renderFacing = new WVec(move.X, move.Y - move.Z, 0).Yaw.Facing;
// Move the missile
var lastPos = pos;
pos += move;
// Check for walls or other blocking obstacles
var shouldExplode = false;
WPos blockedPos;
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, lastPos, pos, info.Width,
info.TargetExtraSearchRadius, out blockedPos))
{
pos = blockedPos;
shouldExplode = true;
}
// Create the smoke trail effect
if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (state != States.Freefall || info.TrailWhenDeactivated))
{
world.AddFrameEndTask(w => w.Add(new SpriteEffect(pos - 3 * move / 2, w, info.TrailImage, info.TrailSequence, trailPalette, false, false, renderFacing)));
ticksToNextSmoke = info.TrailInterval;
}
if (info.ContrailLength > 0)
contrail.Update(pos);
distanceCovered += new WDist(speed);
var cell = world.Map.CellContaining(pos);
var height = world.Map.DistanceAboveTerrain(pos);
shouldExplode |= height.Length < 0 // Hit the ground
|| relTarDist < info.CloseEnough.Length // Within range
|| (info.ExplodeWhenEmpty && rangeLimit >= WDist.Zero && distanceCovered > rangeLimit) // Ran out of fuel
|| !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 && relTarHorDist < info.CloseEnough.Length); // Airburst
if (shouldExplode)
Explode(world);
}
void Explode(World world)
{
if (info.ContrailLength > 0)
world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail)));
world.AddFrameEndTask(w => w.Remove(this));
// Don't blow up in our launcher's face!
if (ticks <= info.Arm)
return;
args.Weapon.Impact(Target.FromPos(pos), args.SourceActor, args.DamageModifiers);
}
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (info.ContrailLength > 0)
yield return contrail;
if (anim == null)
yield break;
var world = args.SourceActor.World;
if (!world.FogObscures(pos))
{
if (info.Shadow)
{
var dat = world.Map.DistanceAboveTerrain(pos);
var shadowPos = pos - new WVec(0, 0, dat.Length);
foreach (var r in anim.Render(shadowPos, wr.Palette("shadow")))
yield return r;
}
var palette = wr.Palette(info.Palette);
foreach (var r in anim.Render(pos, palette))
yield return r;
}
}
}
}