diff --git a/OpenRA.Mods.Common/Graphics/RailgunRenderable.cs b/OpenRA.Mods.Common/Graphics/RailgunRenderable.cs
new file mode 100644
index 0000000000..4bb1856fc3
--- /dev/null
+++ b/OpenRA.Mods.Common/Graphics/RailgunRenderable.cs
@@ -0,0 +1,87 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 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.Graphics;
+using OpenRA.Mods.Common.Projectiles;
+
+namespace OpenRA.Mods.Common.Graphics
+{
+ public struct RailgunHelixRenderable : IRenderable, IFinalizedRenderable
+ {
+ readonly WPos pos;
+ readonly int zOffset;
+ readonly Railgun railgun;
+ readonly RailgunInfo info;
+ readonly WDist helixRadius;
+ readonly int alpha;
+ readonly int ticks;
+
+ WAngle angle;
+
+ public RailgunHelixRenderable(WPos pos, int zOffset, Railgun railgun, RailgunInfo railgunInfo, int ticks)
+ {
+ this.pos = pos;
+ this.zOffset = zOffset;
+ this.railgun = railgun;
+ this.info = railgunInfo;
+ this.ticks = ticks;
+
+ helixRadius = info.HelixRadius + new WDist(ticks * info.HelixRadiusDeltaPerTick);
+ alpha = (railgun.HelixColor.A + ticks * info.HelixAlphaDeltaPerTick).Clamp(0, 255);
+ angle = new WAngle(ticks * info.HelixAngleDeltaPerTick.Angle);
+ }
+
+ public WPos Pos { get { return pos; } }
+ public PaletteReference Palette { get { return null; } }
+ public int ZOffset { get { return zOffset; } }
+ public bool IsDecoration { get { return true; } }
+
+ public IRenderable WithPalette(PaletteReference newPalette) { return new RailgunHelixRenderable(pos, zOffset, railgun, info, ticks); }
+ public IRenderable WithZOffset(int newOffset) { return new RailgunHelixRenderable(pos, newOffset, railgun, info, ticks); }
+ public IRenderable OffsetBy(WVec vec) { return new RailgunHelixRenderable(pos + vec, zOffset, railgun, info, ticks); }
+ public IRenderable AsDecoration() { return this; }
+
+ public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
+ public void Render(WorldRenderer wr)
+ {
+ if (railgun.ForwardStep == WVec.Zero)
+ return;
+
+ var screenWidth = wr.ScreenVector(new WVec(info.HelixThickness.Length, 0, 0))[0];
+
+ // Move forward from self to target to draw helix
+ var centerPos = this.pos;
+ var points = new float3[railgun.CycleCount * info.QuantizationCount];
+ for (var i = points.Length - 1; i >= 0; i--)
+ {
+ // Make it narrower near the end.
+ var rad = i < info.QuantizationCount ? helixRadius / 4 :
+ i < 2 * info.QuantizationCount ? helixRadius / 2 :
+ helixRadius;
+
+ // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x)
+ var u = rad.Length * angle.Cos() * railgun.LeftVector / (1024 * 1024)
+ + rad.Length * angle.Sin() * railgun.UpVector / (1024 * 1024);
+ points[i] = wr.Screen3DPosition(centerPos + u);
+
+ centerPos += railgun.ForwardStep;
+ angle += railgun.AngleStep;
+ }
+
+ Game.Renderer.WorldRgbaColorRenderer.DrawLine(points, screenWidth, Color.FromArgb(alpha, railgun.HelixColor));
+ }
+
+ public void RenderDebugGeometry(WorldRenderer wr) { }
+ public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
+ }
+}
diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
index f251250015..0123851f41 100644
--- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
+++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj
@@ -145,6 +145,7 @@
+
@@ -187,6 +188,7 @@
+
diff --git a/OpenRA.Mods.Common/Projectiles/Railgun.cs b/OpenRA.Mods.Common/Projectiles/Railgun.cs
new file mode 100644
index 0000000000..4fbbb30c8c
--- /dev/null
+++ b/OpenRA.Mods.Common/Projectiles/Railgun.cs
@@ -0,0 +1,239 @@
+#region Copyright & License Information
+/*
+ * Copyright 2007-2017 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.GameRules;
+using OpenRA.Graphics;
+using OpenRA.Mods.Common.Graphics;
+using OpenRA.Mods.Common.Traits;
+using OpenRA.Traits;
+
+namespace OpenRA.Mods.Common.Projectiles
+{
+ [Desc("Laser effect with helix coiling around.")]
+ public class RailgunInfo : IProjectileInfo
+ {
+ [Desc("Damage all units hit by the beam instead of just the target?")]
+ public readonly bool DamageActorsInLine = false;
+
+ [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("Duration of the beam and helix")]
+ public readonly int Duration = 15;
+
+ [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
+ public readonly int ZOffset = 0;
+
+ [Desc("The width of the main trajectory. (\"beam\").")]
+ public readonly WDist BeamWidth = new WDist(86);
+
+ [Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
+ public readonly BeamRenderableShape BeamShape = BeamRenderableShape.Cylindrical;
+
+ [Desc("Beam color in (A),R,G,B.")]
+ public readonly Color BeamColor = Color.FromArgb(128, 255, 255, 255);
+
+ [Desc("When true, this will override BeamColor parameter and draw the laser with player color."
+ + " (Still uses BeamColor's alpha information)")]
+ public readonly bool BeamPlayerColor = false;
+
+ [Desc("Beam alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")]
+ public readonly int BeamAlphaDeltaPerTick = -8;
+
+ [Desc("Thickness of the helix")]
+ public readonly WDist HelixThickness = new WDist(32);
+
+ [Desc("The radius of the spiral effect. (WDist)")]
+ public readonly WDist HelixRadius = new WDist(64);
+
+ [Desc("Height of one complete helix turn, measured parallel to the axis of the helix (WDist)")]
+ public readonly WDist HelixPitch = new WDist(512);
+
+ [Desc("Helix radius gets + this value per tick during drawing")]
+ public readonly int HelixRadiusDeltaPerTick = 8;
+
+ [Desc("Helix alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")]
+ public readonly int HelixAlphaDeltaPerTick = -8;
+
+ [Desc("Helix spins by this much over time each tick.")]
+ public readonly WAngle HelixAngleDeltaPerTick = new WAngle(16);
+
+ [Desc("Draw each cycle of helix with this many quantization steps")]
+ public readonly int QuantizationCount = 16;
+
+ [Desc("Helix color in (A),R,G,B.")]
+ public readonly Color HelixColor = Color.FromArgb(128, 255, 255, 255);
+
+ [Desc("Draw helix in PlayerColor? Overrides RGB part of the HelixColor. (Still uses HelixColor's alpha information)")]
+ public readonly bool HelixPlayerColor = false;
+
+ [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";
+
+ [Desc("Scan radius for actors damaged by beam. If set to zero (default), it will automatically scale to the largest health shape.",
+ "Only set custom values if you know what you're doing.")]
+ public WDist AreaVictimScanRadius = WDist.Zero;
+
+ [Desc("Scan radius for actors with projectile-blocking trait. If set to zero (default), it will automatically scale",
+ "to the blocker with the largest health shape. Only set custom values if you know what you're doing.")]
+ public WDist BlockerScanRadius = WDist.Zero;
+
+ public IProjectile Create(ProjectileArgs args)
+ {
+ var bc = BeamPlayerColor ? Color.FromArgb(BeamColor.A, args.SourceActor.Owner.Color.RGB) : BeamColor;
+ var hc = HelixPlayerColor ? Color.FromArgb(HelixColor.A, args.SourceActor.Owner.Color.RGB) : HelixColor;
+ return new Railgun(args, this, bc, hc);
+ }
+
+ public void RulesetLoaded(Ruleset rules, WeaponInfo wi)
+ {
+ if (BlockerScanRadius == WDist.Zero)
+ BlockerScanRadius = Util.MinimumRequiredBlockerScanRadius(rules);
+
+ if (AreaVictimScanRadius == WDist.Zero)
+ AreaVictimScanRadius = Util.MinimumRequiredVictimScanRadius(rules);
+ }
+ }
+
+ public class Railgun : IProjectile
+ {
+ readonly ProjectileArgs args;
+ readonly RailgunInfo info;
+ readonly Animation hitanim;
+ public readonly Color BeamColor;
+ public readonly Color HelixColor;
+
+ int ticks = 0;
+ bool animationComplete;
+ WPos target;
+
+ // Computing these in Railgun instead of RailgunRenderable saves Info.Duration ticks of computation.
+ // Fortunately, railguns don't track the target.
+ public int CycleCount { get; private set; }
+ public WVec SourceToTarget { get; private set; }
+ public WVec ForwardStep { get; private set; }
+ public WVec LeftVector { get; private set; }
+ public WVec UpVector { get; private set; }
+ public WAngle AngleStep { get; private set; }
+
+ public Railgun(ProjectileArgs args, RailgunInfo info, Color beamColor, Color helixColor)
+ {
+ this.args = args;
+ this.info = info;
+ target = args.PassiveTarget;
+
+ this.BeamColor = beamColor;
+ this.HelixColor = helixColor;
+
+ if (!string.IsNullOrEmpty(info.HitAnim))
+ hitanim = new Animation(args.SourceActor.World, info.HitAnim);
+
+ CalculateVectors();
+ }
+
+ void CalculateVectors()
+ {
+ // Check for blocking actors
+ WPos blockedPos;
+ if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(args.SourceActor.World, target, args.Source,
+ info.BeamWidth, info.BlockerScanRadius, out blockedPos))
+ target = blockedPos;
+
+ // Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x)
+ AngleStep = new WAngle(1024 / info.QuantizationCount);
+
+ SourceToTarget = target - args.Source;
+
+ // Forward step, pointing from src to target.
+ // QuantizationCont * forwardStep == One cycle of beam in src2target direction.
+ ForwardStep = (info.HelixPitch.Length * SourceToTarget) / (info.QuantizationCount * SourceToTarget.Length);
+
+ // An easy vector to find which is perpendicular vector to forwardStep, with 0 Z component
+ LeftVector = new WVec(ForwardStep.Y, -ForwardStep.X, 0);
+ LeftVector = 1024 * LeftVector / LeftVector.Length;
+
+ // Vector that is pointing upwards from the ground
+ UpVector = new WVec(
+ -ForwardStep.X * ForwardStep.Z,
+ -ForwardStep.Z * ForwardStep.Y,
+ ForwardStep.X * ForwardStep.X + ForwardStep.Y * ForwardStep.Y);
+ UpVector = 1024 * UpVector / UpVector.Length;
+
+ //// LeftVector and UpVector are unit vectors of size 1024.
+
+ CycleCount = SourceToTarget.Length / info.HelixPitch.Length;
+ if (SourceToTarget.Length % info.HelixPitch.Length != 0)
+ CycleCount += 1; // math.ceil, int version.
+
+ // Using ForwardStep * CycleCount, the helix and the main beam gets "out of sync"
+ // if drawn from source to target. Instead, the main beam is drawn from source to end point of helix.
+ // Trade-off between computation vs Railgun weapon range.
+ // Modders must not have too large range for railgun weapons.
+ SourceToTarget = info.QuantizationCount * CycleCount * ForwardStep;
+ }
+
+ public void Tick(World world)
+ {
+ if (ticks == 0)
+ {
+ if (hitanim != null)
+ hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true);
+ else
+ animationComplete = true;
+
+ if (!info.DamageActorsInLine)
+ args.Weapon.Impact(Target.FromPos(target), args.SourceActor, args.DamageModifiers);
+ else
+ {
+ var actors = world.FindActorsOnLine(args.Source, target, info.BeamWidth, info.AreaVictimScanRadius);
+ foreach (var a in actors)
+ args.Weapon.Impact(Target.FromActor(a), args.SourceActor, args.DamageModifiers);
+ }
+ }
+
+ if (hitanim != null)
+ hitanim.Tick();
+
+ if (ticks++ > info.Duration && animationComplete)
+ world.AddFrameEndTask(w => w.Remove(this));
+ }
+
+ public IEnumerable Render(WorldRenderer wr)
+ {
+ if (wr.World.FogObscures(target) &&
+ wr.World.FogObscures(args.Source))
+ yield break;
+
+ if (ticks < info.Duration)
+ {
+ yield return new RailgunHelixRenderable(args.Source, info.ZOffset, this, info, ticks);
+ yield return new BeamRenderable(args.Source, info.ZOffset, SourceToTarget, info.BeamShape, info.BeamWidth,
+ Color.FromArgb(BeamColor.A + info.BeamAlphaDeltaPerTick * ticks, BeamColor));
+ }
+
+ if (hitanim != null)
+ foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette)))
+ yield return r;
+ }
+ }
+}
diff --git a/mods/ts/weapons/energyweapons.yaml b/mods/ts/weapons/energyweapons.yaml
index cc97b2aa21..394eaa23ba 100644
--- a/mods/ts/weapons/energyweapons.yaml
+++ b/mods/ts/weapons/energyweapons.yaml
@@ -2,14 +2,13 @@
ReloadDelay: 60
Range: 6c0
Report: bigggun1.aud
- Projectile: AreaBeam
+ Projectile: Railgun
Speed: 20c0
- Duration: 3
- DamageInterval: 2
+ Duration: 15
Width: 80
- BeyondTargetRange: 0c64
Blockable: true
- Color: 0080FFC8
+ DamageActorsInLine: true
+ BeamColor: 0080FFC8
Warhead@1Dam: SpreadDamage
Range: 0, 32
Falloff: 100, 100
@@ -48,8 +47,8 @@ MechRailgun:
Burst: 2 # for alternating muzzle offsets, dmg/s identical to original
BurstDelay: 60
Report: railuse5.aud
- Projectile: AreaBeam
- Color: 00FFFFC8
+ Projectile: Railgun
+ BeamColor: 00FFFFC8
Warhead@1Dam: SpreadDamage
Damage: 200
Versus: