diff --git a/OpenRA.Game/WPos.cs b/OpenRA.Game/WPos.cs
index c1dfc4babc..7d3453ce85 100644
--- a/OpenRA.Game/WPos.cs
+++ b/OpenRA.Game/WPos.cs
@@ -40,6 +40,20 @@ namespace OpenRA
///
public static WPos Lerp(WPos a, WPos b, int mul, int div) { return a + (b - a) * mul / div; }
+ ///
+ /// Returns the linear interpolation between points 'a' and 'b'
+ ///
+ public static WPos Lerp(WPos a, WPos b, long mul, long div)
+ {
+ // The intermediate variables may need more precision than
+ // an int can provide, so we can't use WPos.
+ var x = (int)(a.X + (b.X - a.X) * mul / div);
+ var y = (int)(a.Y + (b.Y - a.Y) * mul / div);
+ var z = (int)(a.Z + (b.Z - a.Z) * mul / div);
+
+ return new WPos(x, y, z);
+ }
+
public static WPos LerpQuadratic(WPos a, WPos b, WAngle pitch, int mul, int div)
{
// Start with a linear lerp between the points
diff --git a/OpenRA.Mods.Common/Effects/AreaBeam.cs b/OpenRA.Mods.Common/Effects/AreaBeam.cs
new file mode 100644
index 0000000000..98d974da6d
--- /dev/null
+++ b/OpenRA.Mods.Common/Effects/AreaBeam.cs
@@ -0,0 +1,217 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * 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.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("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(2048);
+
+ [Desc("Should the beam be visuall rendered? False = Beam is invisible.")]
+ public readonly bool RenderBeam = true;
+
+ [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();
+
+ 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 = OpenRA.Traits.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 = OpenRA.Traits.Util.GetFacing(target - headPos, 0);
+
+ // 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 Render(WorldRenderer wr)
+ {
+ if (!IsBeamComplete && info.RenderBeam && !(wr.World.FogObscures(tailPos) && wr.World.FogObscures(headPos)))
+ {
+ float width, y, z;
+ wr.ScreenVectorComponents(new WVec(info.Width, WDist.Zero, WDist.Zero), out width, out y, out z);
+ var beamRender = new BeamRenderable(headPos, 0, tailPos - headPos, 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;
+ }
+ }
+}
diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index 25653a76b8..f959c07fa8 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -157,6 +157,7 @@
+
@@ -533,6 +534,7 @@
+
diff --git a/OpenRA.Mods.Common/WorldExtensions.cs b/OpenRA.Mods.Common/WorldExtensions.cs
new file mode 100644
index 0000000000..5015813b7c
--- /dev/null
+++ b/OpenRA.Mods.Common/WorldExtensions.cs
@@ -0,0 +1,99 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2015 The OpenRA Developers (see AUTHORS)
+ * This file is part of OpenRA, which is free software. It is made
+ * available to you under the terms of the GNU General Public License
+ * as published by the Free Software Foundation. For more information,
+ * see COPYING.
+ */
+#endregion
+
+using System;
+using System.Collections.Generic;
+using OpenRA.Mods.Common.Traits;
+
+namespace OpenRA.Mods.Common
+{
+ public static class WorldExtensions
+ {
+ ///
+ /// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points.
+ ///
+ /// The engine world the line intersection is to be done in.
+ /// The position the line should start at
+ /// The position the line should end at
+ /// How close an actor's health radius needs to be to the line to be considered 'intersected' by the line
+ /// A list of all the actors intersected by the line
+ public static IEnumerable FindActorsOnLine(this World world, WPos lineStart, WPos lineEnd, WDist lineWidth, WDist targetExtraSearchRadius)
+ {
+ // This line intersection check is done by first just finding all actors within a square that starts at the source, and ends at the target.
+ // Then we iterate over this list, and find all actors for which their health radius is at least within lineWidth of the line.
+ // For actors without a health radius, we simply check their center point.
+ // The square in which we select all actors must be large enough to encompass the entire line's width.
+ var xDir = Math.Sign(lineEnd.X - lineStart.X);
+ var yDir = Math.Sign(lineEnd.Y - lineStart.Y);
+
+ var dir = new WVec(xDir, yDir, 0);
+ var overselect = dir * (1024 + lineWidth.Length + targetExtraSearchRadius.Length);
+ var finalTarget = lineEnd + overselect;
+ var finalSource = lineStart - overselect;
+
+ var actorsInSquare = world.ActorMap.ActorsInBox(finalTarget, finalSource);
+ var intersectedActors = new List();
+
+ foreach (var currActor in actorsInSquare)
+ {
+ var actorWidth = 0;
+ var healthInfo = currActor.Info.TraitInfoOrDefault();
+ if (healthInfo != null)
+ actorWidth = healthInfo.Radius.Length;
+
+ var projection = MinimumPointLineProjection(lineStart, lineEnd, currActor.CenterPosition);
+ var distance = (currActor.CenterPosition - projection).HorizontalLength;
+ var maxReach = actorWidth + lineWidth.Length;
+
+ if (distance <= maxReach)
+ intersectedActors.Add(currActor);
+ }
+
+ return intersectedActors;
+ }
+
+ ///
+ /// Find the point (D) on a line (A-B) that is closest to the target point (C).
+ ///
+ /// The source point (tail) of the line
+ /// The target point (head) of the line
+ /// The target point that the minimum distance should be found to
+ /// The WPos that is the point on the line that is closest to the target point
+ public static WPos MinimumPointLineProjection(WPos lineStart, WPos lineEnd, WPos point)
+ {
+ var squaredLength = (lineEnd - lineStart).HorizontalLengthSquared;
+
+ // Line has zero length, so just use the lineEnd position as the closest position.
+ if (squaredLength == 0)
+ return lineEnd;
+
+ // Consider the line extending the segment, parameterized as target + t (source - target).
+ // We find projection of point onto the line.
+ // It falls where t = [(point - target) . (source - target)] / |source - target|^2
+ // The normal DotProduct math would be (xDiff + yDiff) / dist, where dist = (target - source).LengthSquared;
+ // But in order to avoid floating points, we do not divide here, but rather work with the large numbers as far as possible.
+ // We then later divide by dist, only AFTER we have multiplied by the dotproduct.
+ var xDiff = ((long)point.X - lineEnd.X) * (lineStart.X - lineEnd.X);
+ var yDiff = ((long)point.Y - lineEnd.Y) * (lineStart.Y - lineEnd.Y);
+ var t = xDiff + yDiff;
+
+ // Beyond the 'target' end of the segment
+ if (t < 0)
+ return lineEnd;
+
+ // Beyond the 'source' end of the segment
+ if (t > squaredLength)
+ return lineStart;
+
+ // Projection falls on the segment
+ return WPos.Lerp(lineEnd, lineStart, t, squaredLength);
+ }
+ }
+}