For all projectiles and warheads. Not really a must for everything else, but for CreateEffectWarhead, the ImpactTypes refactor (separate PR) makes it a bit harder to make the warhead valid in every situation, so setting the victim scan to zero is the easiest way to disable scanning for actors completely.
277 lines
9.1 KiB
C#
277 lines
9.1 KiB
C#
#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;
|
|
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, IRulesetLoaded<WeaponInfo>
|
|
{
|
|
[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;
|
|
|
|
[Desc("Scan radius for actors with projectile-blocking trait. If set to a negative value (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 = new WDist(-1);
|
|
|
|
[Desc("Scan radius for actors damaged by beam. If set to a negative value (default), it will automatically scale to the largest health shape.",
|
|
"Only set custom values if you know what you're doing.")]
|
|
public WDist AreaVictimScanRadius = new WDist(-1);
|
|
|
|
public IProjectile Create(ProjectileArgs args)
|
|
{
|
|
var c = UsePlayerColor ? args.SourceActor.Owner.Color.RGB : Color;
|
|
return new AreaBeam(this, args, c);
|
|
}
|
|
|
|
void IRulesetLoaded<WeaponInfo>.RulesetLoaded(Ruleset rules, WeaponInfo wi)
|
|
{
|
|
if (BlockerScanRadius < WDist.Zero)
|
|
BlockerScanRadius = Util.MinimumRequiredBlockerScanRadius(rules);
|
|
|
|
if (AreaVictimScanRadius < WDist.Zero)
|
|
AreaVictimScanRadius = Util.MinimumRequiredVictimScanRadius(rules);
|
|
}
|
|
}
|
|
|
|
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.IsAttacking || 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, info.BlockerScanRadius, 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.AreaVictimScanRadius);
|
|
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;
|
|
}
|
|
}
|
|
}
|