diff --git a/OpenRA.Game/GameRules/WeaponInfo.cs b/OpenRA.Game/GameRules/WeaponInfo.cs index e512c8c153..133d35dc47 100644 --- a/OpenRA.Game/GameRules/WeaponInfo.cs +++ b/OpenRA.Game/GameRules/WeaponInfo.cs @@ -24,6 +24,7 @@ namespace OpenRA.GameRules public int[] RangeModifiers; public int Facing; public WPos Source; + public Func CurrentSource; public Actor SourceActor; public WPos PassiveTarget; public Target GuidedTarget; 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 a753d8c425..aa7610e841 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -157,6 +157,7 @@ + @@ -534,6 +535,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index 4b86abe678..3c64116f34 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -187,7 +187,7 @@ namespace OpenRA.Mods.Common.Traits return null; var barrel = Barrels[Burst % Barrels.Length]; - var muzzlePosition = self.CenterPosition + MuzzleOffset(self, barrel); + Func muzzlePosition = () => self.CenterPosition + MuzzleOffset(self, barrel); var legacyFacing = MuzzleOrientation(self, barrel).Yaw.Angle / 4; var args = new ProjectileArgs @@ -204,7 +204,8 @@ namespace OpenRA.Mods.Common.Traits RangeModifiers = self.TraitsImplementing() .Select(a => a.GetRangeModifier()).ToArray(), - Source = muzzlePosition, + Source = muzzlePosition(), + CurrentSource = muzzlePosition, SourceActor = self, PassiveTarget = target.CenterPosition, GuidedTarget = target diff --git a/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs b/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs index 3bd90c95a7..7f599b8db2 100644 --- a/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs +++ b/OpenRA.Mods.Common/Traits/BlocksProjectiles.cs @@ -34,5 +34,31 @@ namespace OpenRA.Mods.Common.Traits .Where(t => t.Info.Height.Length >= dat.Length) .Any(Exts.IsTraitEnabled)); } + + public static bool AnyBlockingActorsBetween(World world, WPos start, WPos end, WDist width, WDist overscan, out WPos hit) + { + var actors = world.FindActorsOnLine(start, end, width, overscan); + var length = (end - start).Length; + + foreach (var a in actors) + { + var blockers = a.TraitsImplementing() + .Where(Exts.IsTraitEnabled).ToList(); + + if (!blockers.Any()) + continue; + + var hitPos = WorldExtensions.MinimumPointLineProjection(start, end, a.CenterPosition); + var dat = world.Map.DistanceAboveTerrain(hitPos); + if ((hitPos - start).Length < length && blockers.Any(t => t.Info.Height.Length >= dat.Length)) + { + hit = hitPos; + return true; + } + } + + hit = WPos.Zero; + return false; + } } } diff --git a/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs b/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs index f0ea965dd9..b18935a65e 100644 --- a/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs +++ b/OpenRA.Mods.Common/Traits/ThrowsShrapnel.cs @@ -70,6 +70,7 @@ namespace OpenRA.Mods.Common.Traits .Select(a => a.GetRangeModifier()).ToArray(), Source = self.CenterPosition, + CurrentSource = () => self.CenterPosition, SourceActor = self, PassiveTarget = self.CenterPosition + new WVec(range, 0, 0).Rotate(rotation) }; 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); + } + } +} diff --git a/OpenRA.Mods.D2k/Traits/SpiceBloom.cs b/OpenRA.Mods.D2k/Traits/SpiceBloom.cs index baeb4ae926..07280e08ca 100644 --- a/OpenRA.Mods.D2k/Traits/SpiceBloom.cs +++ b/OpenRA.Mods.D2k/Traits/SpiceBloom.cs @@ -136,6 +136,7 @@ namespace OpenRA.Mods.D2k.Traits .Select(a => a.GetInaccuracyModifier()).ToArray(), Source = self.CenterPosition, + CurrentSource = () => self.CenterPosition, SourceActor = self, PassiveTarget = self.World.Map.CenterOfCell(cell.Value) }; diff --git a/mods/d2k/rules/vehicles.yaml b/mods/d2k/rules/vehicles.yaml index 53d1b2c322..dc2e1f3200 100644 --- a/mods/d2k/rules/vehicles.yaml +++ b/mods/d2k/rules/vehicles.yaml @@ -290,6 +290,8 @@ sonic_tank: Actor: sonic_tank.husk AttractsWorms: Intensity: 600 + Targetable: + TargetTypes: Ground, Vehicle, C4, Sonictank devastator: Inherits: ^Tank diff --git a/mods/d2k/weapons.yaml b/mods/d2k/weapons.yaml index 538c1789f2..e64b932e28 100644 --- a/mods/d2k/weapons.yaml +++ b/mods/d2k/weapons.yaml @@ -530,25 +530,44 @@ Sound: ReloadDelay: 90 Range: 5c0 Report: SONIC1.WAV - Projectile: LaserZap - BeamWidth: 10 - BeamDuration: 8 - UsePlayerColor: true + Projectile: AreaBeam + Speed: 0c128 + Duration: 4 # Has a length of 0c512 + DamageInterval: 3 # Travels 0c384 between impacts, will hit a target roughly three times + Width: 0c512 + Falloff: 100, 100, 50 + Range: 0, 6c0, 11c0 + BeyondTargetRange: 1c0 + Color: 00FFFFC8 Warhead@1Dam: SpreadDamage - Spread: 256 - Falloff: 100, 100, 0 - Damage: 500 #80 D2k but damages through all in path + Range: 0, 32 + Falloff: 100, 100 + Damage: 150 + AffectsParent: false + ValidStances: Neutral, Enemy Versus: wall: 50 building: 60 heavy: 60 invulnerable: 0 - cy: 20 - harvester: 50 + cy: 20 + harvester: 50 + DamageTypes: Prone50Percent, TriggerProne, SoundDeath + Warhead@2Dam: SpreadDamage + Range: 0, 32 + Falloff: 50, 50 # Only does half damage to friendly units + Damage: 150 + InvalidTargets: Sonictank # Does not affect friendly sonic tanks at all + AffectsParent: false + ValidStances: Ally + Versus: + wall: 50 + building: 60 + heavy: 60 + invulnerable: 0 + cy: 20 + harvester: 50 DamageTypes: Prone50Percent, TriggerProne, SoundDeath - Warhead@3Eff: CreateEffect - ImpactSound: SONIC3.WAV - Delay: 10 Heal: ReloadDelay: 160 diff --git a/mods/ts/rules/gdi-vehicles.yaml b/mods/ts/rules/gdi-vehicles.yaml index 04c0f0a2c2..be713edeba 100644 --- a/mods/ts/rules/gdi-vehicles.yaml +++ b/mods/ts/rules/gdi-vehicles.yaml @@ -207,6 +207,8 @@ SONIC: Queue: Vehicle BuildPaletteOrder: 70 Prerequisites: ~gaweap, gatech + Targetable: + TargetTypes: Ground, Vehicle, Repair, Disruptor Mobile: ROT: 4 Speed: 56 diff --git a/mods/ts/weapons/energyweapons.yaml b/mods/ts/weapons/energyweapons.yaml index 736f89a5ae..494974e11d 100644 --- a/mods/ts/weapons/energyweapons.yaml +++ b/mods/ts/weapons/energyweapons.yaml @@ -2,14 +2,35 @@ LtRail: ReloadDelay: 60 Range: 6c0 Report: BIGGGUN1.AUD - Projectile: LaserZap - Speed: 1c682 - BeamWidth: 1 - BeamDuration: 10 + Projectile: AreaBeam + Speed: 20c0 + Duration: 3 + DamageInterval: 2 + Width: 0c128 + BeyondTargetRange: 0c64 + Blockable: true Color: 0080FFC8 Warhead@1Dam: SpreadDamage - Spread: 42 + Range: 0, 32 + Falloff: 100, 100 Damage: 150 + InfDeath: 6 + AffectsParent: false + ValidStances: Neutral, Enemy + Versus: + None: 100 + Wood: 130 + Light: 150 + Heavy: 110 + Concrete: 5 + DamageTypes: Prone100Percent, TriggerProne, SmallExplosionDeath + Warhead@2Dam: SpreadDamage + Range: 0, 32 + Falloff: 50, 50 # Only does half damage to friendly units + Damage: 150 + InfDeath: 6 + AffectsParent: false + ValidStances: Ally Versus: None: 100 Wood: 130 @@ -24,9 +45,14 @@ MechRailgun: Burst: 2 # for alternating muzzle offsets, dmg/s identical to original BurstDelay: 60 Report: RAILUSE5.AUD - Projectile: LaserZap + Projectile: AreaBeam + Speed: 20c0 + Duration: 3 + DamageInterval: 2 + Width: 0c128 + BeyondTargetRange: 0c64 + Blockable: true Color: 00FFFFC8 - BeamWidth: 3 Warhead@1Dam: SpreadDamage Spread: 42 Damage: 200 @@ -39,21 +65,39 @@ MechRailgun: DamageTypes: Prone100Percent, TriggerProne, FireDeath SonicZap: - ReloadDelay: 120 + ReloadDelay: 180 Range: 6c0 Charges: yes Report: SONIC4.AUD - Projectile: LaserZap + Projectile: AreaBeam + Speed: 0c128 + Duration: 90 + DamageInterval: 5 # Roughly 18 impacts. + Width: 0c384 + BeyondTargetRange: 0c256 + Blockable: true Color: 00FFFFC8 - BeamWidth: 12 - BeamDuration: 50 Warhead@1Dam: SpreadDamage - Spread: 42 - Damage: 100 + Range: 0, 32 + Falloff: 100, 100 + Damage: 8 + AffectsParent: false + ValidStances: Neutral, Enemy Versus: Heavy: 80 Concrete: 60 - DamageTypes: Prone50Percent, TriggerProne, FireDeath + DamageTypes: Prone50Percent, TriggerProne, ExplosionDeath + Warhead@2Dam: SpreadDamage + Range: 0, 32 + Falloff: 50, 50 + Damage: 8 + InvalidTargets: Disruptor # Does not affect friendly disruptors at all + AffectsParent: false + ValidStances: Ally + Versus: + Heavy: 80 + Concrete: 60 + DamageTypes: Prone50Percent, TriggerProne, ExplosionDeath CyCannon: ReloadDelay: 50