penev discovered that the RulesetLoaded functions of projectiles were never being called, meaning that their blocking calculations were not properly accounting for actors with large hitboxes. The best fix for this is to change FindActorsOnLine to always account for the largest actor's hit radius, rather than forcing callers to pass the largest radius. Per the comment in Util.cs, as a result, move this computation to ActorMap. I decided to simplify by not making a separate calculation for actors that block projectiles only; this may cause a small performance degradation as the search space is a bit larger. Similarly to this, I've removed the ability to specify a search radius manually. Because this is only a search radius, setting a value smaller than the largest eligible actor makes no sense; that would lead to completely inconsistent blocking. Setting a larger value, on the other hand, would make no difference. CreateEffectWarhead was the only place in core code any of these search radii were set, and that's because 0 was a mysterious magic value that made the warhead incapable of hitting actors. I replaced it with a boolean flag that more clearly indicates the actual behaviour. Fixes #14151.
260 lines
8.2 KiB
C#
260 lines
8.2 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2018 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;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using OpenRA.Effects;
|
|
using OpenRA.GameRules;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Effects;
|
|
using OpenRA.Mods.Common.Graphics;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Projectiles
|
|
{
|
|
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("The shape of the beam. Accepts values Cylindrical or Flat.")]
|
|
public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical;
|
|
|
|
[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("Does the beam follow the target.")]
|
|
public readonly bool TrackTarget = false;
|
|
|
|
[Desc("Should the beam be visually rendered? False = Beam is invisible.")]
|
|
public readonly bool RenderBeam = true;
|
|
|
|
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
|
public readonly int ZOffset = 0;
|
|
|
|
[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 IProjectile Create(ProjectileArgs args)
|
|
{
|
|
var c = UsePlayerColor ? args.SourceActor.Owner.Color.RGB : Color;
|
|
return new AreaBeam(this, args, c);
|
|
}
|
|
}
|
|
|
|
public class AreaBeam : IProjectile, 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 continueTracking = true;
|
|
|
|
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 = 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 = (target - headPos).Yaw.Facing;
|
|
|
|
// 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);
|
|
}
|
|
|
|
void TrackTarget()
|
|
{
|
|
if (!continueTracking)
|
|
return;
|
|
|
|
if (args.GuidedTarget.IsValidFor(args.SourceActor))
|
|
{
|
|
var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source);
|
|
var targetDistance = new WDist((guidedTargetPos - args.Source).Length);
|
|
|
|
// Only continue tracking target if it's within weapon range +
|
|
// BeyondTargetRange to avoid edge case stuttering (start firing and immediately stop again).
|
|
if (targetDistance > args.Weapon.Range + info.BeyondTargetRange)
|
|
StopTargeting();
|
|
else
|
|
{
|
|
target = guidedTargetPos;
|
|
towardsTargetFacing = (target - args.Source).Yaw.Facing;
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StopTargeting()
|
|
{
|
|
continueTracking = false;
|
|
isTailTravelling = true;
|
|
}
|
|
|
|
public void Tick(World world)
|
|
{
|
|
if (info.TrackTarget)
|
|
TrackTarget();
|
|
|
|
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 leniency to avoid edge case stuttering (start firing and immediately stop again).
|
|
var outOfWeaponRange = args.Weapon.Range + info.BeyondTargetRange < new WDist((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.IsAiming || outOfWeaponRange)
|
|
StopTargeting();
|
|
|
|
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, 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);
|
|
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)))
|
|
{
|
|
var beamRender = new BeamRenderable(headPos, info.ZOffset, tailPos - headPos, info.Shape, info.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;
|
|
}
|
|
}
|
|
}
|