From f7f0dde4bbca7e2b0accf78bab8830be3b36127d Mon Sep 17 00:00:00 2001 From: UberWaffe Date: Wed, 13 Aug 2014 21:30:43 +0200 Subject: [PATCH] Added LineImpactProjectile. --- OpenRA.Game/WPos.cs | 14 ++ OpenRA.Mods.Common/Effects/AreaBeam.cs | 217 +++++++++++++++++++ OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 2 + OpenRA.Mods.Common/WorldExtensions.cs | 99 +++++++++ 4 files changed, 332 insertions(+) create mode 100644 OpenRA.Mods.Common/Effects/AreaBeam.cs create mode 100644 OpenRA.Mods.Common/WorldExtensions.cs 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); + } + } +}