Merge pull request #9357 from UberWaffe/LineImpactProjectile

Added a LineImpactProjectile (Attempt 2)
This commit is contained in:
Matthias Mailänder
2015-12-05 18:09:12 +01:00
13 changed files with 457 additions and 28 deletions

View File

@@ -24,6 +24,7 @@ namespace OpenRA.GameRules
public int[] RangeModifiers; public int[] RangeModifiers;
public int Facing; public int Facing;
public WPos Source; public WPos Source;
public Func<WPos> CurrentSource;
public Actor SourceActor; public Actor SourceActor;
public WPos PassiveTarget; public WPos PassiveTarget;
public Target GuidedTarget; public Target GuidedTarget;

View File

@@ -40,6 +40,20 @@ namespace OpenRA
/// </summary> /// </summary>
public static WPos Lerp(WPos a, WPos b, int mul, int div) { return a + (b - a) * mul / div; } public static WPos Lerp(WPos a, WPos b, int mul, int div) { return a + (b - a) * mul / div; }
/// <summary>
/// Returns the linear interpolation between points 'a' and 'b'
/// </summary>
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) public static WPos LerpQuadratic(WPos a, WPos b, WAngle pitch, int mul, int div)
{ {
// Start with a linear lerp between the points // Start with a linear lerp between the points

View File

@@ -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<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 = 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<IRenderable> 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;
}
}
}

View File

@@ -157,6 +157,7 @@
<Compile Include="Effects\FloatingText.cs" /> <Compile Include="Effects\FloatingText.cs" />
<Compile Include="Effects\GravityBomb.cs" /> <Compile Include="Effects\GravityBomb.cs" />
<Compile Include="Effects\LaserZap.cs" /> <Compile Include="Effects\LaserZap.cs" />
<Compile Include="Effects\AreaBeam.cs" />
<Compile Include="Effects\Missile.cs" /> <Compile Include="Effects\Missile.cs" />
<Compile Include="Effects\NukeLaunch.cs" /> <Compile Include="Effects\NukeLaunch.cs" />
<Compile Include="Effects\PowerdownIndicator.cs" /> <Compile Include="Effects\PowerdownIndicator.cs" />
@@ -534,6 +535,7 @@
<Compile Include="UtilityCommands\ExtractLanguageStringsCommand.cs" /> <Compile Include="UtilityCommands\ExtractLanguageStringsCommand.cs" />
<Compile Include="UtilityCommands\ExtractLuaDocsCommand.cs" /> <Compile Include="UtilityCommands\ExtractLuaDocsCommand.cs" />
<Compile Include="UtilityCommands\ExtractTraitDocsCommand.cs" /> <Compile Include="UtilityCommands\ExtractTraitDocsCommand.cs" />
<Compile Include="WorldExtensions.cs" />
<Compile Include="UtilityCommands\GenerateMinimapCommand.cs" /> <Compile Include="UtilityCommands\GenerateMinimapCommand.cs" />
<Compile Include="UtilityCommands\GetMapHashCommand.cs" /> <Compile Include="UtilityCommands\GetMapHashCommand.cs" />
<Compile Include="UtilityCommands\Glob.cs" /> <Compile Include="UtilityCommands\Glob.cs" />

View File

@@ -187,7 +187,7 @@ namespace OpenRA.Mods.Common.Traits
return null; return null;
var barrel = Barrels[Burst % Barrels.Length]; var barrel = Barrels[Burst % Barrels.Length];
var muzzlePosition = self.CenterPosition + MuzzleOffset(self, barrel); Func<WPos> muzzlePosition = () => self.CenterPosition + MuzzleOffset(self, barrel);
var legacyFacing = MuzzleOrientation(self, barrel).Yaw.Angle / 4; var legacyFacing = MuzzleOrientation(self, barrel).Yaw.Angle / 4;
var args = new ProjectileArgs var args = new ProjectileArgs
@@ -204,7 +204,8 @@ namespace OpenRA.Mods.Common.Traits
RangeModifiers = self.TraitsImplementing<IRangeModifier>() RangeModifiers = self.TraitsImplementing<IRangeModifier>()
.Select(a => a.GetRangeModifier()).ToArray(), .Select(a => a.GetRangeModifier()).ToArray(),
Source = muzzlePosition, Source = muzzlePosition(),
CurrentSource = muzzlePosition,
SourceActor = self, SourceActor = self,
PassiveTarget = target.CenterPosition, PassiveTarget = target.CenterPosition,
GuidedTarget = target GuidedTarget = target

View File

@@ -34,5 +34,31 @@ namespace OpenRA.Mods.Common.Traits
.Where(t => t.Info.Height.Length >= dat.Length) .Where(t => t.Info.Height.Length >= dat.Length)
.Any(Exts.IsTraitEnabled)); .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<BlocksProjectiles>()
.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;
}
} }
} }

View File

@@ -70,6 +70,7 @@ namespace OpenRA.Mods.Common.Traits
.Select(a => a.GetRangeModifier()).ToArray(), .Select(a => a.GetRangeModifier()).ToArray(),
Source = self.CenterPosition, Source = self.CenterPosition,
CurrentSource = () => self.CenterPosition,
SourceActor = self, SourceActor = self,
PassiveTarget = self.CenterPosition + new WVec(range, 0, 0).Rotate(rotation) PassiveTarget = self.CenterPosition + new WVec(range, 0, 0).Rotate(rotation)
}; };

View File

@@ -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
{
/// <summary>
/// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points.
/// </summary>
/// <param name="world">The engine world the line intersection is to be done in.</param>
/// <param name="lineStart">The position the line should start at</param>
/// <param name="lineEnd">The position the line should end at</param>
/// <param name="lineWidth">How close an actor's health radius needs to be to the line to be considered 'intersected' by the line</param>
/// <returns>A list of all the actors intersected by the line</returns>
public static IEnumerable<Actor> 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<Actor>();
foreach (var currActor in actorsInSquare)
{
var actorWidth = 0;
var healthInfo = currActor.Info.TraitInfoOrDefault<HealthInfo>();
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;
}
/// <summary>
/// Find the point (D) on a line (A-B) that is closest to the target point (C).
/// </summary>
/// <param name="lineStart">The source point (tail) of the line</param>
/// <param name="lineEnd">The target point (head) of the line</param>
/// <param name="point">The target point that the minimum distance should be found to</param>
/// <returns>The WPos that is the point on the line that is closest to the target point</returns>
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);
}
}
}

View File

@@ -136,6 +136,7 @@ namespace OpenRA.Mods.D2k.Traits
.Select(a => a.GetInaccuracyModifier()).ToArray(), .Select(a => a.GetInaccuracyModifier()).ToArray(),
Source = self.CenterPosition, Source = self.CenterPosition,
CurrentSource = () => self.CenterPosition,
SourceActor = self, SourceActor = self,
PassiveTarget = self.World.Map.CenterOfCell(cell.Value) PassiveTarget = self.World.Map.CenterOfCell(cell.Value)
}; };

View File

@@ -290,6 +290,8 @@ sonic_tank:
Actor: sonic_tank.husk Actor: sonic_tank.husk
AttractsWorms: AttractsWorms:
Intensity: 600 Intensity: 600
Targetable:
TargetTypes: Ground, Vehicle, C4, Sonictank
devastator: devastator:
Inherits: ^Tank Inherits: ^Tank

View File

@@ -530,25 +530,44 @@ Sound:
ReloadDelay: 90 ReloadDelay: 90
Range: 5c0 Range: 5c0
Report: SONIC1.WAV Report: SONIC1.WAV
Projectile: LaserZap Projectile: AreaBeam
BeamWidth: 10 Speed: 0c128
BeamDuration: 8 Duration: 4 # Has a length of 0c512
UsePlayerColor: true 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 Warhead@1Dam: SpreadDamage
Spread: 256 Range: 0, 32
Falloff: 100, 100, 0 Falloff: 100, 100
Damage: 500 #80 D2k but damages through all in path Damage: 150
AffectsParent: false
ValidStances: Neutral, Enemy
Versus: Versus:
wall: 50 wall: 50
building: 60 building: 60
heavy: 60 heavy: 60
invulnerable: 0 invulnerable: 0
cy: 20 cy: 20
harvester: 50 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 DamageTypes: Prone50Percent, TriggerProne, SoundDeath
Warhead@3Eff: CreateEffect
ImpactSound: SONIC3.WAV
Delay: 10
Heal: Heal:
ReloadDelay: 160 ReloadDelay: 160

View File

@@ -207,6 +207,8 @@ SONIC:
Queue: Vehicle Queue: Vehicle
BuildPaletteOrder: 70 BuildPaletteOrder: 70
Prerequisites: ~gaweap, gatech Prerequisites: ~gaweap, gatech
Targetable:
TargetTypes: Ground, Vehicle, Repair, Disruptor
Mobile: Mobile:
ROT: 4 ROT: 4
Speed: 56 Speed: 56

View File

@@ -2,14 +2,35 @@ LtRail:
ReloadDelay: 60 ReloadDelay: 60
Range: 6c0 Range: 6c0
Report: BIGGGUN1.AUD Report: BIGGGUN1.AUD
Projectile: LaserZap Projectile: AreaBeam
Speed: 1c682 Speed: 20c0
BeamWidth: 1 Duration: 3
BeamDuration: 10 DamageInterval: 2
Width: 0c128
BeyondTargetRange: 0c64
Blockable: true
Color: 0080FFC8 Color: 0080FFC8
Warhead@1Dam: SpreadDamage Warhead@1Dam: SpreadDamage
Spread: 42 Range: 0, 32
Falloff: 100, 100
Damage: 150 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: Versus:
None: 100 None: 100
Wood: 130 Wood: 130
@@ -24,9 +45,14 @@ MechRailgun:
Burst: 2 # for alternating muzzle offsets, dmg/s identical to original Burst: 2 # for alternating muzzle offsets, dmg/s identical to original
BurstDelay: 60 BurstDelay: 60
Report: RAILUSE5.AUD Report: RAILUSE5.AUD
Projectile: LaserZap Projectile: AreaBeam
Speed: 20c0
Duration: 3
DamageInterval: 2
Width: 0c128
BeyondTargetRange: 0c64
Blockable: true
Color: 00FFFFC8 Color: 00FFFFC8
BeamWidth: 3
Warhead@1Dam: SpreadDamage Warhead@1Dam: SpreadDamage
Spread: 42 Spread: 42
Damage: 200 Damage: 200
@@ -39,21 +65,39 @@ MechRailgun:
DamageTypes: Prone100Percent, TriggerProne, FireDeath DamageTypes: Prone100Percent, TriggerProne, FireDeath
SonicZap: SonicZap:
ReloadDelay: 120 ReloadDelay: 180
Range: 6c0 Range: 6c0
Charges: yes Charges: yes
Report: SONIC4.AUD Report: SONIC4.AUD
Projectile: LaserZap Projectile: AreaBeam
Speed: 0c128
Duration: 90
DamageInterval: 5 # Roughly 18 impacts.
Width: 0c384
BeyondTargetRange: 0c256
Blockable: true
Color: 00FFFFC8 Color: 00FFFFC8
BeamWidth: 12
BeamDuration: 50
Warhead@1Dam: SpreadDamage Warhead@1Dam: SpreadDamage
Spread: 42 Range: 0, 32
Damage: 100 Falloff: 100, 100
Damage: 8
AffectsParent: false
ValidStances: Neutral, Enemy
Versus: Versus:
Heavy: 80 Heavy: 80
Concrete: 60 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: CyCannon:
ReloadDelay: 50 ReloadDelay: 50