Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
297
OpenRA.Mods.Common/Projectiles/AreaBeam.cs
Normal file
297
OpenRA.Mods.Common/Projectiles/AreaBeam.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Beam projectile that travels in a straight line.")]
|
||||
public class AreaBeamInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("Projectile speed in WDist / tick, two values indicate a randomly picked velocity per beam.")]
|
||||
public readonly ImmutableArray<WDist> Speed = [new(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(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(0);
|
||||
|
||||
[Desc("The minimum distance the beam travels.")]
|
||||
public readonly WDist MinDistance = WDist.Zero;
|
||||
|
||||
[Desc("Damage modifier applied at each range step.")]
|
||||
public readonly ImmutableArray<int> Falloff = [100, 100];
|
||||
|
||||
[Desc("Ranges at which each Falloff step is defined.")]
|
||||
public readonly ImmutableArray<WDist> Range = [WDist.Zero, new(int.MaxValue)];
|
||||
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
||||
|
||||
[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.OwnerColor() : 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;
|
||||
readonly WDist weaponRange;
|
||||
|
||||
[VerifySync]
|
||||
WPos headPos;
|
||||
|
||||
[VerifySync]
|
||||
WPos tailPos;
|
||||
|
||||
[VerifySync]
|
||||
WPos target;
|
||||
|
||||
int length;
|
||||
WAngle towardsTargetFacing;
|
||||
int headTicks;
|
||||
int tailTicks;
|
||||
bool isHeadTravelling = true;
|
||||
bool isTailTravelling;
|
||||
bool continueTracking = true;
|
||||
|
||||
bool IsBeamComplete => !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 maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args);
|
||||
target += WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
}
|
||||
|
||||
towardsTargetFacing = (target - headPos).Yaw;
|
||||
|
||||
// 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.FromYaw(towardsTargetFacing));
|
||||
var dist = (args.SourceActor.CenterPosition - target).Length;
|
||||
int extraDist;
|
||||
if (info.MinDistance.Length > dist)
|
||||
{
|
||||
if (info.MinDistance.Length - dist < info.BeyondTargetRange.Length)
|
||||
extraDist = info.BeyondTargetRange.Length;
|
||||
else
|
||||
extraDist = info.MinDistance.Length - dist;
|
||||
}
|
||||
else
|
||||
extraDist = info.BeyondTargetRange.Length;
|
||||
|
||||
target += dir * extraDist / 1024;
|
||||
|
||||
length = Math.Max((target - headPos).Length / speed.Length, 1);
|
||||
weaponRange = new WDist(Util.ApplyPercentageModifiers(args.Weapon.Range.Length, args.RangeModifiers));
|
||||
}
|
||||
|
||||
void TrackTarget()
|
||||
{
|
||||
if (!continueTracking)
|
||||
return;
|
||||
|
||||
if (args.GuidedTarget.IsValidFor(args.SourceActor))
|
||||
{
|
||||
var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(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 > weaponRange + info.BeyondTargetRange)
|
||||
StopTargeting();
|
||||
else
|
||||
{
|
||||
target = guidedTargetPos;
|
||||
towardsTargetFacing = (target - args.Source).Yaw;
|
||||
|
||||
// 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.FromYaw(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 = weaponRange + 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
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, tailPos, headPos, info.Width, out var 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));
|
||||
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(args.Source, target), args.CurrentMuzzleFacing()),
|
||||
|
||||
// Calculating an impact position is bogus for line damage.
|
||||
// FindActorsOnLine guarantees that the beam touches the target's HitShape,
|
||||
// so we just assume a center hit to avoid bogus warhead recalculations.
|
||||
ImpactPosition = a.CenterPosition,
|
||||
DamageModifiers = adjustedModifiers.ToArray(),
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromActor(a), warheadArgs);
|
||||
}
|
||||
}
|
||||
|
||||
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 [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;
|
||||
}
|
||||
}
|
||||
}
|
||||
397
OpenRA.Mods.Common/Projectiles/Bullet.cs
Normal file
397
OpenRA.Mods.Common/Projectiles/Bullet.cs
Normal file
@@ -0,0 +1,397 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Effects;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Projectile that travels in a straight line or arc.")]
|
||||
public class BulletInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")]
|
||||
public readonly ImmutableArray<WDist> Speed = [new(17)];
|
||||
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
||||
|
||||
[Desc("Image to display.")]
|
||||
public readonly string Image = null;
|
||||
|
||||
[SequenceReference(nameof(Image), allowNullImage: true)]
|
||||
[Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")]
|
||||
public readonly ImmutableArray<string> Sequences = ["idle"];
|
||||
|
||||
[PaletteReference(nameof(IsPlayerPalette))]
|
||||
[Desc("The palette used to draw this projectile.")]
|
||||
public readonly string Palette = "effect";
|
||||
|
||||
[Desc("Palette is a player palette BaseName")]
|
||||
public readonly bool IsPlayerPalette = false;
|
||||
|
||||
[Desc("Does this projectile have a shadow?")]
|
||||
public readonly bool Shadow = false;
|
||||
|
||||
[Desc("Color to draw shadow if Shadow is true.")]
|
||||
public readonly Color ShadowColor = Color.FromArgb(140, 0, 0, 0);
|
||||
|
||||
[Desc("Trail animation.")]
|
||||
public readonly string TrailImage = null;
|
||||
|
||||
[SequenceReference(nameof(TrailImage), allowNullImage: true)]
|
||||
[Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")]
|
||||
public readonly ImmutableArray<string> TrailSequences = ["idle"];
|
||||
|
||||
[Desc("Interval in ticks between each spawned Trail animation.")]
|
||||
public readonly int TrailInterval = 2;
|
||||
|
||||
[Desc("Delay in ticks until trail animation is spawned.")]
|
||||
public readonly int TrailDelay = 1;
|
||||
|
||||
[PaletteReference(nameof(TrailUsePlayerPalette))]
|
||||
[Desc("Palette used to render the trail sequence.")]
|
||||
public readonly string TrailPalette = "effect";
|
||||
|
||||
[Desc("Use the Player Palette to render the trail sequence.")]
|
||||
public readonly bool TrailUsePlayerPalette = false;
|
||||
|
||||
[Desc("Is this blocked by actors with BlocksProjectiles trait.")]
|
||||
public readonly bool Blockable = true;
|
||||
|
||||
[Desc("Width of projectile (used for finding blocking actors).")]
|
||||
public readonly WDist Width = new(1);
|
||||
|
||||
[Desc("Arc in WAngles, two values indicate variable arc.")]
|
||||
public readonly ImmutableArray<WAngle> LaunchAngle = [WAngle.Zero];
|
||||
|
||||
[Desc("Up to how many times does this bullet bounce when touching ground without hitting a target.",
|
||||
"0 implies exploding on contact with the originally targeted position.")]
|
||||
public readonly int BounceCount = 0;
|
||||
|
||||
[Desc("Modify distance of each bounce by this percentage of previous distance.")]
|
||||
public readonly int BounceRangeModifier = 60;
|
||||
|
||||
[Desc("Sound to play when the projectile hits the ground, but not the target.")]
|
||||
public readonly string BounceSound = null;
|
||||
|
||||
[Desc("Terrain where the projectile explodes instead of bouncing.")]
|
||||
public readonly FrozenSet<string> InvalidBounceTerrain = FrozenSet<string>.Empty;
|
||||
|
||||
[Desc("Trigger the explosion if the projectile touches an actor thats owner has these player relationships.")]
|
||||
public readonly PlayerRelationship ValidBounceBlockerRelationships = PlayerRelationship.Enemy | PlayerRelationship.Neutral;
|
||||
|
||||
[Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")]
|
||||
public readonly WDist AirburstAltitude = WDist.Zero;
|
||||
|
||||
[Desc("When set, display a line behind the actor. Length is measured in ticks after appearing.")]
|
||||
public readonly int ContrailLength = 0;
|
||||
|
||||
[Desc("Time (in ticks) after which the line should appear. Controls the distance to the actor.")]
|
||||
public readonly int ContrailDelay = 1;
|
||||
|
||||
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
||||
public readonly int ContrailZOffset = 2047;
|
||||
|
||||
[Desc("Thickness of the emitted line at the start of the contrail.")]
|
||||
public readonly WDist ContrailStartWidth = new(64);
|
||||
|
||||
[Desc("Thickness of the emitted line at the end of the contrail. Will default to " + nameof(ContrailStartWidth) + " if left undefined")]
|
||||
public readonly WDist? ContrailEndWidth = null;
|
||||
|
||||
[Desc("RGB color at the contrail start.")]
|
||||
public readonly Color ContrailStartColor = Color.White;
|
||||
|
||||
[Desc("Use player remap color instead of a custom color at the contrail the start.")]
|
||||
public readonly bool ContrailStartColorUsePlayerColor = false;
|
||||
|
||||
[Desc("The alpha value [from 0 to 255] of color at the contrail the start.")]
|
||||
public readonly int ContrailStartColorAlpha = 255;
|
||||
|
||||
[Desc("RGB color at the contrail end. Will default to " + nameof(ContrailStartColor) + " if left undefined")]
|
||||
public readonly Color? ContrailEndColor;
|
||||
|
||||
[Desc("Use player remap color instead of a custom color at the contrail end.")]
|
||||
public readonly bool ContrailEndColorUsePlayerColor = false;
|
||||
|
||||
[Desc("The alpha value [from 0 to 255] of color at the contrail end.")]
|
||||
public readonly int ContrailEndColorAlpha = 0;
|
||||
|
||||
public virtual IProjectile Create(ProjectileArgs args) { return new Bullet(this, args); }
|
||||
}
|
||||
|
||||
public class Bullet : IProjectile, ISync
|
||||
{
|
||||
readonly BulletInfo info;
|
||||
protected readonly ProjectileArgs Args;
|
||||
protected readonly Animation Animation;
|
||||
readonly WAngle facing;
|
||||
readonly WAngle angle;
|
||||
readonly WDist speed;
|
||||
readonly string trailPalette;
|
||||
|
||||
readonly float3 shadowColor;
|
||||
readonly float shadowAlpha;
|
||||
|
||||
readonly ContrailRenderable contrail;
|
||||
|
||||
[VerifySync]
|
||||
protected WPos pos, lastPos, target, source;
|
||||
|
||||
int length;
|
||||
int ticks, smokeTicks;
|
||||
int remainingBounces;
|
||||
|
||||
protected bool FlightLengthReached => ticks >= length;
|
||||
|
||||
public Bullet(BulletInfo info, ProjectileArgs args)
|
||||
{
|
||||
this.info = info;
|
||||
Args = args;
|
||||
pos = args.Source;
|
||||
source = args.Source;
|
||||
|
||||
var world = args.SourceActor.World;
|
||||
|
||||
if (info.LaunchAngle.Length > 1)
|
||||
angle = new WAngle(world.SharedRandom.Next(info.LaunchAngle[0].Angle, info.LaunchAngle[1].Angle));
|
||||
else
|
||||
angle = info.LaunchAngle[0];
|
||||
|
||||
if (info.Speed.Length > 1)
|
||||
speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length));
|
||||
else
|
||||
speed = info.Speed[0];
|
||||
|
||||
target = args.PassiveTarget;
|
||||
if (info.Inaccuracy.Length > 0)
|
||||
{
|
||||
var maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args);
|
||||
target += WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
}
|
||||
|
||||
if (info.AirburstAltitude > WDist.Zero)
|
||||
target += new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude);
|
||||
|
||||
facing = (target - pos).Yaw;
|
||||
length = Math.Max((target - pos).Length / speed.Length, 1);
|
||||
|
||||
if (!string.IsNullOrEmpty(info.Image))
|
||||
{
|
||||
Animation = new Animation(world, info.Image, new Func<WAngle>(GetEffectiveFacing));
|
||||
Animation.PlayRepeating(info.Sequences.Random(world.SharedRandom));
|
||||
}
|
||||
|
||||
if (info.ContrailLength > 0)
|
||||
{
|
||||
var startcolor = Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor);
|
||||
var endcolor = Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor);
|
||||
contrail = new ContrailRenderable(world, args.SourceActor,
|
||||
startcolor, info.ContrailStartColorUsePlayerColor,
|
||||
endcolor, info.ContrailEndColor == null ? info.ContrailStartColorUsePlayerColor : info.ContrailEndColorUsePlayerColor,
|
||||
info.ContrailStartWidth,
|
||||
info.ContrailEndWidth ?? info.ContrailStartWidth,
|
||||
info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
|
||||
}
|
||||
|
||||
trailPalette = info.TrailPalette;
|
||||
if (info.TrailUsePlayerPalette)
|
||||
trailPalette += args.SourceActor.Owner.InternalName;
|
||||
|
||||
smokeTicks = info.TrailDelay;
|
||||
remainingBounces = info.BounceCount;
|
||||
|
||||
shadowColor = new float3(info.ShadowColor.R, info.ShadowColor.G, info.ShadowColor.B) / 255f;
|
||||
shadowAlpha = info.ShadowColor.A / 255f;
|
||||
}
|
||||
|
||||
WAngle GetEffectiveFacing()
|
||||
{
|
||||
var at = (float)ticks / (length - 1);
|
||||
var attitude = angle.Tan() * (1 - 2 * at) / (4 * 1024);
|
||||
|
||||
var u = facing.Angle % 512 / 512f;
|
||||
var scale = 2048 * u * (1 - u);
|
||||
|
||||
var effective = (int)(facing.Angle < 512
|
||||
? facing.Angle - scale * attitude
|
||||
: facing.Angle + scale * attitude);
|
||||
|
||||
return new WAngle(effective);
|
||||
}
|
||||
|
||||
public virtual void Tick(World world)
|
||||
{
|
||||
Animation?.Tick();
|
||||
|
||||
lastPos = pos;
|
||||
pos = WPos.LerpQuadratic(source, target, angle, ticks, length);
|
||||
|
||||
if (ShouldExplode(world))
|
||||
{
|
||||
if (info.ContrailLength > 0)
|
||||
world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail)));
|
||||
|
||||
Explode(world);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldExplode(World world)
|
||||
{
|
||||
// Check for walls or other blocking obstacles
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, Args.SourceActor.Owner, lastPos, pos, info.Width, out var blockedPos))
|
||||
{
|
||||
pos = blockedPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.TrailImage) && --smokeTicks < 0)
|
||||
{
|
||||
var delayedPos = WPos.LerpQuadratic(source, target, angle, ticks - info.TrailDelay, length);
|
||||
world.AddFrameEndTask(w => w.Add(new SpriteEffect(delayedPos, GetEffectiveFacing(), w,
|
||||
info.TrailImage, info.TrailSequences.Random(world.SharedRandom), trailPalette)));
|
||||
|
||||
smokeTicks = info.TrailInterval;
|
||||
}
|
||||
|
||||
if (info.ContrailLength > 0)
|
||||
contrail.Update(pos);
|
||||
|
||||
var flightLengthReached = ticks++ >= length;
|
||||
var shouldBounce = remainingBounces > 0;
|
||||
|
||||
if (flightLengthReached && shouldBounce)
|
||||
{
|
||||
var cell = world.Map.CellContaining(pos);
|
||||
if (!world.Map.Contains(cell))
|
||||
return true;
|
||||
|
||||
if (info.InvalidBounceTerrain.Contains(world.Map.GetTerrainInfo(cell).Type))
|
||||
return true;
|
||||
|
||||
if (AnyValidTargetsInRadius(world, pos, info.Width, Args.SourceActor, true))
|
||||
return true;
|
||||
|
||||
target += (pos - source) * info.BounceRangeModifier / 100;
|
||||
var dat = world.Map.DistanceAboveTerrain(target);
|
||||
target += new WVec(0, 0, -dat.Length);
|
||||
length = Math.Max((target - pos).Length / speed.Length, 1);
|
||||
|
||||
ticks = 0;
|
||||
source = pos;
|
||||
Game.Sound.Play(SoundType.World, info.BounceSound, source);
|
||||
remainingBounces--;
|
||||
}
|
||||
|
||||
// Flight length reached / exceeded
|
||||
if (flightLengthReached && !shouldBounce)
|
||||
return true;
|
||||
|
||||
// Driving into cell with higher height level
|
||||
if (world.Map.DistanceAboveTerrain(pos).Length < 0)
|
||||
return true;
|
||||
|
||||
// After first bounce, check for targets each tick
|
||||
if (remainingBounces < info.BounceCount && AnyValidTargetsInRadius(world, pos, info.Width, Args.SourceActor, true))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (info.ContrailLength > 0)
|
||||
yield return contrail;
|
||||
|
||||
if (FlightLengthReached)
|
||||
yield break;
|
||||
|
||||
foreach (var r in RenderAnimation(wr))
|
||||
yield return r;
|
||||
}
|
||||
|
||||
protected IEnumerable<IRenderable> RenderAnimation(WorldRenderer wr)
|
||||
{
|
||||
if (Animation == null)
|
||||
yield break;
|
||||
|
||||
var world = Args.SourceActor.World;
|
||||
if (!world.FogObscures(pos))
|
||||
{
|
||||
var paletteName = info.Palette;
|
||||
if (paletteName != null && info.IsPlayerPalette)
|
||||
paletteName += Args.SourceActor.Owner.InternalName;
|
||||
|
||||
var palette = wr.Palette(paletteName);
|
||||
|
||||
if (info.Shadow)
|
||||
{
|
||||
var dat = world.Map.DistanceAboveTerrain(pos);
|
||||
var shadowPos = pos - new WVec(0, 0, dat.Length);
|
||||
foreach (var r in Animation.Render(shadowPos, palette))
|
||||
yield return ((IModifyableRenderable)r)
|
||||
.WithTint(shadowColor, ((IModifyableRenderable)r).TintModifiers | TintModifiers.ReplaceColor)
|
||||
.WithAlpha(shadowAlpha);
|
||||
}
|
||||
|
||||
foreach (var r in Animation.Render(pos, palette))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Explode(World world)
|
||||
{
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
|
||||
var warheadArgs = new WarheadArgs(Args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(lastPos, pos), Args.Facing),
|
||||
ImpactPosition = pos,
|
||||
};
|
||||
|
||||
Args.Weapon.Impact(Target.FromPos(pos), warheadArgs);
|
||||
}
|
||||
|
||||
bool AnyValidTargetsInRadius(World world, WPos pos, WDist radius, Actor firedBy, bool checkTargetType)
|
||||
{
|
||||
foreach (var victim in world.FindActorsOnCircle(pos, radius))
|
||||
{
|
||||
if (checkTargetType && !Target.FromActor(victim).IsValidFor(firedBy))
|
||||
continue;
|
||||
|
||||
if (victim != Args.GuidedTarget.Actor && !info.ValidBounceBlockerRelationships.HasRelationship(firedBy.Owner.RelationshipWith(victim.Owner)))
|
||||
continue;
|
||||
|
||||
// If the impact position is within any actor's HitShape, we have a direct hit
|
||||
// PERF: Avoid using TraitsImplementing<HitShape> that needs to find the actor in the trait dictionary.
|
||||
foreach (var targetPos in victim.EnabledTargetablePositions)
|
||||
if (targetPos is HitShape h && h.DistanceFromEdge(victim, pos).Length <= 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
146
OpenRA.Mods.Common/Projectiles/GravityBomb.cs
Normal file
146
OpenRA.Mods.Common/Projectiles/GravityBomb.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Projectile with customisable acceleration vector.")]
|
||||
public class GravityBombInfo : IProjectileInfo
|
||||
{
|
||||
public readonly string Image = null;
|
||||
|
||||
[SequenceReference(nameof(Image), allowNullImage: true)]
|
||||
[Desc("Loop a randomly chosen sequence of Image from this list while falling.")]
|
||||
public readonly ImmutableArray<string> Sequences = ["idle"];
|
||||
|
||||
[SequenceReference(nameof(Image), allowNullImage: true)]
|
||||
[Desc("Sequence to play when launched. Skipped if null or empty.")]
|
||||
public readonly string OpenSequence = null;
|
||||
|
||||
[PaletteReference]
|
||||
[Desc("The palette used to draw this projectile.")]
|
||||
public readonly string Palette = "effect";
|
||||
|
||||
[Desc("Palette is a player palette BaseName")]
|
||||
public readonly bool IsPlayerPalette = false;
|
||||
|
||||
[Desc("Does this projectile have a shadow?")]
|
||||
public readonly bool Shadow = false;
|
||||
|
||||
[Desc("Color to draw shadow if Shadow is true.")]
|
||||
public readonly Color ShadowColor = Color.FromArgb(140, 0, 0, 0);
|
||||
|
||||
[Desc("Projectile movement vector per tick (forward, right, up), use negative values for opposite directions.")]
|
||||
public readonly WVec Velocity = WVec.Zero;
|
||||
|
||||
[Desc("Value added to Velocity every tick.")]
|
||||
public readonly WVec Acceleration = new(0, 0, -15);
|
||||
|
||||
public IProjectile Create(ProjectileArgs args) { return new GravityBomb(this, args); }
|
||||
}
|
||||
|
||||
public class GravityBomb : IProjectile, ISync
|
||||
{
|
||||
readonly GravityBombInfo info;
|
||||
readonly Animation anim;
|
||||
readonly ProjectileArgs args;
|
||||
readonly WVec acceleration;
|
||||
|
||||
readonly float3 shadowColor;
|
||||
readonly float shadowAlpha;
|
||||
|
||||
WVec velocity;
|
||||
|
||||
[VerifySync]
|
||||
WPos pos, lastPos;
|
||||
|
||||
public GravityBomb(GravityBombInfo info, ProjectileArgs args)
|
||||
{
|
||||
this.info = info;
|
||||
this.args = args;
|
||||
pos = args.Source;
|
||||
var convertedVelocity = new WVec(info.Velocity.Y, -info.Velocity.X, info.Velocity.Z);
|
||||
velocity = convertedVelocity.Rotate(WRot.FromYaw(args.Facing));
|
||||
acceleration = new WVec(info.Acceleration.Y, -info.Acceleration.X, info.Acceleration.Z);
|
||||
|
||||
if (!string.IsNullOrEmpty(info.Image))
|
||||
{
|
||||
anim = new Animation(args.SourceActor.World, info.Image, () => args.Facing);
|
||||
|
||||
if (!string.IsNullOrEmpty(info.OpenSequence))
|
||||
anim.PlayThen(info.OpenSequence, () => anim.PlayRepeating(info.Sequences.Random(args.SourceActor.World.SharedRandom)));
|
||||
else
|
||||
anim.PlayRepeating(info.Sequences.Random(args.SourceActor.World.SharedRandom));
|
||||
}
|
||||
|
||||
shadowColor = new float3(info.ShadowColor.R, info.ShadowColor.G, info.ShadowColor.B) / 255f;
|
||||
shadowAlpha = info.ShadowColor.A / 255f;
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
lastPos = pos;
|
||||
pos += velocity;
|
||||
velocity += acceleration;
|
||||
|
||||
if (pos.Z <= args.PassiveTarget.Z)
|
||||
{
|
||||
pos += new WVec(0, 0, args.PassiveTarget.Z - pos.Z);
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(lastPos, pos), args.Facing),
|
||||
ImpactPosition = pos,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromPos(pos), warheadArgs);
|
||||
}
|
||||
|
||||
anim?.Tick();
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (anim == null)
|
||||
yield break;
|
||||
|
||||
var world = args.SourceActor.World;
|
||||
if (!world.FogObscures(pos))
|
||||
{
|
||||
var paletteName = info.Palette;
|
||||
if (paletteName != null && info.IsPlayerPalette)
|
||||
paletteName += args.SourceActor.Owner.InternalName;
|
||||
|
||||
var palette = wr.Palette(paletteName);
|
||||
|
||||
if (info.Shadow)
|
||||
{
|
||||
var dat = world.Map.DistanceAboveTerrain(pos);
|
||||
var shadowPos = pos - new WVec(0, 0, dat.Length);
|
||||
foreach (var r in anim.Render(shadowPos, palette))
|
||||
yield return ((IModifyableRenderable)r)
|
||||
.WithTint(shadowColor, ((IModifyableRenderable)r).TintModifiers | TintModifiers.ReplaceColor)
|
||||
.WithAlpha(shadowAlpha);
|
||||
}
|
||||
|
||||
foreach (var r in anim.Render(pos, palette))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
OpenRA.Mods.Common/Projectiles/InstantHit.cs
Normal file
96
OpenRA.Mods.Common/Projectiles/InstantHit.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Instant, invisible, usually direct-on-target projectile.")]
|
||||
public class InstantHitInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
||||
|
||||
[Desc("Projectile can be blocked.")]
|
||||
public readonly bool Blockable = false;
|
||||
|
||||
[Desc("The width of the projectile.")]
|
||||
public readonly WDist Width = new(1);
|
||||
|
||||
[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 readonly WDist BlockerScanRadius = new(-1);
|
||||
|
||||
public IProjectile Create(ProjectileArgs args) { return new InstantHit(this, args); }
|
||||
}
|
||||
|
||||
public class InstantHit : IProjectile
|
||||
{
|
||||
readonly ProjectileArgs args;
|
||||
readonly InstantHitInfo info;
|
||||
|
||||
Target target;
|
||||
|
||||
public InstantHit(InstantHitInfo info, ProjectileArgs args)
|
||||
{
|
||||
this.args = args;
|
||||
this.info = info;
|
||||
|
||||
if (args.Weapon.TargetActorCenter)
|
||||
target = args.GuidedTarget;
|
||||
else if (info.Inaccuracy.Length > 0)
|
||||
{
|
||||
var maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args);
|
||||
var inaccuracyOffset = WVec.FromPDF(args.SourceActor.World.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
target = Target.FromPos(args.PassiveTarget + inaccuracyOffset);
|
||||
}
|
||||
else
|
||||
target = Target.FromPos(args.PassiveTarget);
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
// If GuidedTarget has become invalid due to getting killed the same tick,
|
||||
// we need to set target to args.PassiveTarget to prevent target.CenterPosition below from crashing.
|
||||
if (target.Type == TargetType.Invalid)
|
||||
target = Target.FromPos(args.PassiveTarget);
|
||||
|
||||
// Check for blocking actors
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(
|
||||
world, args.SourceActor.Owner, args.Source, target.CenterPosition, info.Width, out var blockedPos))
|
||||
target = Target.FromPos(blockedPos);
|
||||
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(args.Source, target.CenterPosition), args.Facing),
|
||||
ImpactPosition = target.CenterPosition,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(target, warheadArgs);
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
217
OpenRA.Mods.Common/Projectiles/LaserZap.cs
Normal file
217
OpenRA.Mods.Common/Projectiles/LaserZap.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Effects;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Not a sprite, but an engine effect.")]
|
||||
public class LaserZapInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("The width of the zap.")]
|
||||
public readonly WDist Width = new(86);
|
||||
|
||||
[Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
|
||||
public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical;
|
||||
|
||||
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
||||
public readonly int ZOffset = 0;
|
||||
|
||||
[Desc("The maximum duration (in ticks) of the beam's existence.")]
|
||||
public readonly int Duration = 10;
|
||||
|
||||
[Desc("Total time-frame in ticks that the beam deals damage every DamageInterval.")]
|
||||
public readonly int DamageDuration = 1;
|
||||
|
||||
[Desc("The number of ticks between the beam causing warhead impacts in its area of effect.")]
|
||||
public readonly int DamageInterval = 1;
|
||||
|
||||
public readonly bool UsePlayerColor = false;
|
||||
|
||||
[Desc("Color of the beam.")]
|
||||
public readonly Color Color = Color.Red;
|
||||
|
||||
[Desc("Beam follows the target.")]
|
||||
public readonly bool TrackTarget = true;
|
||||
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
||||
|
||||
[Desc("Beam can be blocked.")]
|
||||
public readonly bool Blockable = false;
|
||||
|
||||
[Desc("Draw a second beam (for 'glow' effect).")]
|
||||
public readonly bool SecondaryBeam = false;
|
||||
|
||||
[Desc("The width of the zap.")]
|
||||
public readonly WDist SecondaryBeamWidth = new(86);
|
||||
|
||||
[Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
|
||||
public readonly BeamRenderableShape SecondaryBeamShape = BeamRenderableShape.Cylindrical;
|
||||
|
||||
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
||||
public readonly int SecondaryBeamZOffset = 0;
|
||||
|
||||
public readonly bool SecondaryBeamUsePlayerColor = false;
|
||||
|
||||
[Desc("Color of the secondary beam.")]
|
||||
public readonly Color SecondaryBeamColor = Color.Red;
|
||||
|
||||
[Desc("Impact animation.")]
|
||||
public readonly string HitAnim = null;
|
||||
|
||||
[SequenceReference(nameof(HitAnim), allowNullImage: true)]
|
||||
[Desc("Sequence of impact animation to use.")]
|
||||
public readonly string HitAnimSequence = "idle";
|
||||
|
||||
[PaletteReference]
|
||||
public readonly string HitAnimPalette = "effect";
|
||||
|
||||
[Desc("Image containing launch effect sequence.")]
|
||||
public readonly string LaunchEffectImage = null;
|
||||
|
||||
[SequenceReference(nameof(LaunchEffectImage), allowNullImage: true)]
|
||||
[Desc("Launch effect sequence to play.")]
|
||||
public readonly string LaunchEffectSequence = null;
|
||||
|
||||
[PaletteReference]
|
||||
[Desc("Palette to use for launch effect.")]
|
||||
public readonly string LaunchEffectPalette = "effect";
|
||||
|
||||
public IProjectile Create(ProjectileArgs args)
|
||||
{
|
||||
var c = UsePlayerColor ? args.SourceActor.OwnerColor() : Color;
|
||||
return new LaserZap(this, args, c);
|
||||
}
|
||||
}
|
||||
|
||||
public class LaserZap : IProjectile, ISync
|
||||
{
|
||||
readonly ProjectileArgs args;
|
||||
readonly LaserZapInfo info;
|
||||
readonly Animation hitanim;
|
||||
readonly Color color;
|
||||
readonly Color secondaryColor;
|
||||
readonly bool hasLaunchEffect;
|
||||
int ticks;
|
||||
int interval;
|
||||
bool showHitAnim;
|
||||
|
||||
[VerifySync]
|
||||
WPos target;
|
||||
|
||||
[VerifySync]
|
||||
WPos source;
|
||||
|
||||
public LaserZap(LaserZapInfo info, ProjectileArgs args, Color color)
|
||||
{
|
||||
this.args = args;
|
||||
this.info = info;
|
||||
this.color = color;
|
||||
secondaryColor = info.SecondaryBeamUsePlayerColor ? args.SourceActor.OwnerColor() : info.SecondaryBeamColor;
|
||||
target = args.PassiveTarget;
|
||||
source = args.Source;
|
||||
|
||||
if (info.Inaccuracy.Length > 0)
|
||||
{
|
||||
var maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args);
|
||||
target += WVec.FromPDF(args.SourceActor.World.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.HitAnim))
|
||||
{
|
||||
hitanim = new Animation(args.SourceActor.World, info.HitAnim);
|
||||
showHitAnim = true;
|
||||
}
|
||||
|
||||
hasLaunchEffect = !string.IsNullOrEmpty(info.LaunchEffectImage) && !string.IsNullOrEmpty(info.LaunchEffectSequence);
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
source = args.CurrentSource();
|
||||
|
||||
if (hasLaunchEffect && ticks == 0)
|
||||
world.AddFrameEndTask(w => w.Add(new SpriteEffect(args.CurrentSource, args.CurrentMuzzleFacing, world,
|
||||
info.LaunchEffectImage, info.LaunchEffectSequence, info.LaunchEffectPalette)));
|
||||
|
||||
// Beam tracks target
|
||||
if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor))
|
||||
target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(source);
|
||||
|
||||
// Check for blocking actors
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, source, target, info.Width, out var blockedPos))
|
||||
{
|
||||
target = blockedPos;
|
||||
}
|
||||
|
||||
if (ticks < info.DamageDuration && --interval <= 0)
|
||||
{
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(source, target), args.CurrentMuzzleFacing()),
|
||||
ImpactPosition = target,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromPos(target), warheadArgs);
|
||||
interval = info.DamageInterval;
|
||||
}
|
||||
|
||||
if (showHitAnim)
|
||||
{
|
||||
if (ticks == 0)
|
||||
hitanim.PlayThen(info.HitAnimSequence, () => showHitAnim = false);
|
||||
|
||||
hitanim.Tick();
|
||||
}
|
||||
|
||||
if (++ticks >= info.Duration && !showHitAnim)
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (wr.World.FogObscures(target) &&
|
||||
wr.World.FogObscures(source))
|
||||
yield break;
|
||||
|
||||
if (ticks < info.Duration)
|
||||
{
|
||||
var rc = Color.FromArgb((info.Duration - ticks) * color.A / info.Duration, color);
|
||||
yield return new BeamRenderable(source, info.ZOffset, target - source, info.Shape, info.Width, rc);
|
||||
|
||||
if (info.SecondaryBeam)
|
||||
{
|
||||
var src = Color.FromArgb((info.Duration - ticks) * secondaryColor.A / info.Duration, secondaryColor);
|
||||
yield return new BeamRenderable(source, info.SecondaryBeamZOffset, target - source,
|
||||
info.SecondaryBeamShape, info.SecondaryBeamWidth, src);
|
||||
}
|
||||
}
|
||||
|
||||
if (showHitAnim)
|
||||
foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette)))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
980
OpenRA.Mods.Common/Projectiles/Missile.cs
Normal file
980
OpenRA.Mods.Common/Projectiles/Missile.cs
Normal file
@@ -0,0 +1,980 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Effects;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Projectile with smart tracking.")]
|
||||
public class MissileInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("Name of the image containing the projectile sequence.")]
|
||||
public readonly string Image = null;
|
||||
|
||||
[SequenceReference(nameof(Image), allowNullImage: true)]
|
||||
[Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")]
|
||||
public readonly ImmutableArray<string> Sequences = ["idle"];
|
||||
|
||||
[PaletteReference(nameof(IsPlayerPalette))]
|
||||
[Desc("Palette used to render the projectile sequence.")]
|
||||
public readonly string Palette = "effect";
|
||||
|
||||
[Desc("Palette is a player palette BaseName")]
|
||||
public readonly bool IsPlayerPalette = false;
|
||||
|
||||
[Desc("Does this projectile have a shadow?")]
|
||||
public readonly bool Shadow = false;
|
||||
|
||||
[Desc("Color to draw shadow if Shadow is true.")]
|
||||
public readonly Color ShadowColor = Color.FromArgb(140, 0, 0, 0);
|
||||
|
||||
[Desc("Minimum vertical launch angle (pitch).")]
|
||||
public readonly WAngle MinimumLaunchAngle = new(-64);
|
||||
|
||||
[Desc("Maximum vertical launch angle (pitch).")]
|
||||
public readonly WAngle MaximumLaunchAngle = new(128);
|
||||
|
||||
[Desc("Minimum launch speed in WDist / tick. Defaults to Speed if -1.")]
|
||||
public readonly WDist MinimumLaunchSpeed = new(-1);
|
||||
|
||||
[Desc("Maximum launch speed in WDist / tick. Defaults to Speed if -1.")]
|
||||
public readonly WDist MaximumLaunchSpeed = new(-1);
|
||||
|
||||
[Desc("Maximum projectile speed in WDist / tick")]
|
||||
public readonly WDist Speed = new(384);
|
||||
|
||||
[Desc("Projectile acceleration when propulsion activated.")]
|
||||
public readonly WDist Acceleration = new(5);
|
||||
|
||||
[Desc("How many ticks before this missile is armed and can explode.")]
|
||||
public readonly int Arm = 0;
|
||||
|
||||
[Desc("Is the missile blocked by actors with BlocksProjectiles: trait.")]
|
||||
public readonly bool Blockable = true;
|
||||
|
||||
[Desc("Is the missile aware of terrain height levels. Only needed for mods with real, non-visual height levels.")]
|
||||
public readonly bool TerrainHeightAware = false;
|
||||
|
||||
[Desc("Width of projectile (used for finding blocking actors).")]
|
||||
public readonly WDist Width = new(1);
|
||||
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Absolute;
|
||||
|
||||
[Desc("Inaccuracy override when successfully locked onto target. Defaults to Inaccuracy if negative.")]
|
||||
public readonly WDist LockOnInaccuracy = new(-1);
|
||||
|
||||
[Desc("Probability of locking onto and following target.")]
|
||||
public readonly int LockOnProbability = 100;
|
||||
|
||||
[Desc("Horizontal rate of turn.")]
|
||||
public readonly WAngle HorizontalRateOfTurn = new(20);
|
||||
|
||||
[Desc("Vertical rate of turn.")]
|
||||
public readonly WAngle VerticalRateOfTurn = new(24);
|
||||
|
||||
[Desc("Gravity applied while in free fall.")]
|
||||
public readonly int Gravity = 10;
|
||||
|
||||
[Desc("Run out of fuel after covering this distance. Zero for defaulting to weapon range. Negative for unlimited fuel.")]
|
||||
public readonly WDist RangeLimit = WDist.Zero;
|
||||
|
||||
[Desc("Explode when running out of fuel.")]
|
||||
public readonly bool ExplodeWhenEmpty = true;
|
||||
|
||||
[Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")]
|
||||
public readonly WDist AirburstAltitude = WDist.Zero;
|
||||
|
||||
[Desc("Cruise altitude. Zero means no cruise altitude used.")]
|
||||
public readonly WDist CruiseAltitude = new(512);
|
||||
|
||||
[Desc("Activate homing mechanism after this many ticks.")]
|
||||
public readonly int HomingActivationDelay = 0;
|
||||
|
||||
[Desc("Image that contains the trail animation.")]
|
||||
public readonly string TrailImage = null;
|
||||
|
||||
[SequenceReference(nameof(TrailImage), allowNullImage: true)]
|
||||
[Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")]
|
||||
public readonly ImmutableArray<string> TrailSequences = ["idle"];
|
||||
|
||||
[PaletteReference(nameof(TrailUsePlayerPalette))]
|
||||
[Desc("Palette used to render the trail sequence.")]
|
||||
public readonly string TrailPalette = "effect";
|
||||
|
||||
[Desc("Use the Player Palette to render the trail sequence.")]
|
||||
public readonly bool TrailUsePlayerPalette = false;
|
||||
|
||||
[Desc("Interval in ticks between spawning trail animation.")]
|
||||
public readonly int TrailInterval = 2;
|
||||
|
||||
[Desc("Should trail animation be spawned when the propulsion is not activated.")]
|
||||
public readonly bool TrailWhenDeactivated = false;
|
||||
|
||||
[Desc("When set, display a line behind the actor. Length is measured in ticks after appearing.")]
|
||||
public readonly int ContrailLength = 0;
|
||||
|
||||
[Desc("Time (in ticks) after which the line should appear. Controls the distance to the actor.")]
|
||||
public readonly int ContrailDelay = 1;
|
||||
|
||||
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
||||
public readonly int ContrailZOffset = 2047;
|
||||
|
||||
[Desc("Thickness of the emitted line at the start of the contrail.")]
|
||||
public readonly WDist ContrailStartWidth = new(64);
|
||||
|
||||
[Desc("Thickness of the emitted line at the end of the contrail. Will default to " + nameof(ContrailStartWidth) + " if left undefined")]
|
||||
public readonly WDist? ContrailEndWidth = null;
|
||||
|
||||
[Desc("RGB color at the contrail start.")]
|
||||
public readonly Color ContrailStartColor = Color.White;
|
||||
|
||||
[Desc("Use player remap color instead of a custom color at the contrail the start.")]
|
||||
public readonly bool ContrailStartColorUsePlayerColor = false;
|
||||
|
||||
[Desc("The alpha value [from 0 to 255] of color at the contrail the start.")]
|
||||
public readonly int ContrailStartColorAlpha = 255;
|
||||
|
||||
[Desc("RGB color at the contrail end. Will default to " + nameof(ContrailStartColor) + " if left undefined")]
|
||||
public readonly Color? ContrailEndColor;
|
||||
|
||||
[Desc("Use player remap color instead of a custom color at the contrail end.")]
|
||||
public readonly bool ContrailEndColorUsePlayerColor = false;
|
||||
|
||||
[Desc("The alpha value [from 0 to 255] of color at the contrail end.")]
|
||||
public readonly int ContrailEndColorAlpha = 0;
|
||||
|
||||
[Desc("Should missile targeting be thrown off by nearby actors with JamsMissiles.")]
|
||||
public readonly bool Jammable = true;
|
||||
|
||||
[Desc("Range of facings by which jammed missiles can stray from current path.")]
|
||||
public readonly int JammedDiversionRange = 20;
|
||||
|
||||
[Desc("Explodes when leaving the following terrain type, e.g., Water for torpedoes.")]
|
||||
public readonly string BoundToTerrainType = "";
|
||||
|
||||
[Desc("Allow the missile to snap to the target, meaning jumping to the target immediately when",
|
||||
"the missile enters the radius of the current speed around the target.")]
|
||||
public readonly bool AllowSnapping = false;
|
||||
|
||||
[Desc("Explodes when inside this proximity radius to target.",
|
||||
"Note: If this value is lower than the missile speed, this check might",
|
||||
"not trigger fast enough, causing the missile to fly past the target.")]
|
||||
public readonly WDist CloseEnough = new(298);
|
||||
|
||||
public IProjectile Create(ProjectileArgs args) { return new Missile(this, args); }
|
||||
}
|
||||
|
||||
// TODO: double check square roots!!!
|
||||
public class Missile : IProjectile, ISync
|
||||
{
|
||||
enum States
|
||||
{
|
||||
Freefall,
|
||||
Homing,
|
||||
Hitting
|
||||
}
|
||||
|
||||
readonly MissileInfo info;
|
||||
readonly ProjectileArgs args;
|
||||
readonly Animation anim;
|
||||
|
||||
readonly WVec gravity;
|
||||
readonly int minLaunchSpeed;
|
||||
readonly int maxLaunchSpeed;
|
||||
readonly int maxSpeed;
|
||||
readonly WAngle minLaunchAngle;
|
||||
readonly WAngle maxLaunchAngle;
|
||||
|
||||
readonly float3 shadowColor;
|
||||
readonly float shadowAlpha;
|
||||
|
||||
int ticks;
|
||||
|
||||
int ticksToNextSmoke;
|
||||
readonly ContrailRenderable contrail;
|
||||
readonly string trailPalette;
|
||||
|
||||
States state;
|
||||
bool targetPassedBy;
|
||||
readonly bool lockOn;
|
||||
bool allowPassBy; // TODO: use this also with high minimum launch angle settings
|
||||
|
||||
WPos targetPosition;
|
||||
readonly WVec offset;
|
||||
|
||||
WVec tarVel;
|
||||
WVec predVel;
|
||||
|
||||
[VerifySync]
|
||||
WPos pos;
|
||||
|
||||
WVec velocity;
|
||||
int speed;
|
||||
int loopRadius;
|
||||
WDist distanceCovered;
|
||||
readonly WDist rangeLimit;
|
||||
|
||||
WAngle renderFacing;
|
||||
|
||||
[VerifySync]
|
||||
int hFacing;
|
||||
|
||||
[VerifySync]
|
||||
int vFacing;
|
||||
|
||||
public Missile(MissileInfo info, ProjectileArgs args)
|
||||
{
|
||||
this.info = info;
|
||||
this.args = args;
|
||||
|
||||
pos = args.Source;
|
||||
hFacing = args.Facing.Facing;
|
||||
gravity = new WVec(0, 0, -info.Gravity);
|
||||
targetPosition = args.PassiveTarget;
|
||||
var limit = info.RangeLimit != WDist.Zero ? info.RangeLimit : args.Weapon.Range;
|
||||
rangeLimit = new WDist(Util.ApplyPercentageModifiers(limit.Length, args.RangeModifiers));
|
||||
minLaunchSpeed = info.MinimumLaunchSpeed.Length > -1 ? info.MinimumLaunchSpeed.Length : info.Speed.Length;
|
||||
maxLaunchSpeed = info.MaximumLaunchSpeed.Length > -1 ? info.MaximumLaunchSpeed.Length : info.Speed.Length;
|
||||
maxSpeed = info.Speed.Length;
|
||||
minLaunchAngle = info.MinimumLaunchAngle;
|
||||
maxLaunchAngle = info.MaximumLaunchAngle;
|
||||
|
||||
// Make sure the projectile on being spawned is approximately looking at the correct direction.
|
||||
renderFacing = args.Facing;
|
||||
|
||||
var world = args.SourceActor.World;
|
||||
|
||||
if (world.SharedRandom.Next(100) <= info.LockOnProbability)
|
||||
lockOn = true;
|
||||
|
||||
var inaccuracy = lockOn && info.LockOnInaccuracy.Length > -1 ? info.LockOnInaccuracy.Length : info.Inaccuracy.Length;
|
||||
if (inaccuracy > 0)
|
||||
{
|
||||
var maxInaccuracyOffset = Util.GetProjectileInaccuracy(inaccuracy, info.InaccuracyType, args);
|
||||
offset = WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
}
|
||||
|
||||
DetermineLaunchSpeedAndAngle(world, out speed, out vFacing);
|
||||
|
||||
velocity = new WVec(0, -speed, 0)
|
||||
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)));
|
||||
|
||||
if (!string.IsNullOrEmpty(info.Image))
|
||||
{
|
||||
anim = new Animation(world, info.Image, () => renderFacing);
|
||||
anim.PlayRepeating(info.Sequences.Random(world.SharedRandom));
|
||||
}
|
||||
|
||||
if (info.ContrailLength > 0)
|
||||
{
|
||||
var startcolor = Color.FromArgb(info.ContrailStartColorAlpha, info.ContrailStartColor);
|
||||
var endcolor = Color.FromArgb(info.ContrailEndColorAlpha, info.ContrailEndColor ?? startcolor);
|
||||
contrail = new ContrailRenderable(world, args.SourceActor,
|
||||
startcolor, info.ContrailStartColorUsePlayerColor,
|
||||
endcolor, info.ContrailEndColor == null ? info.ContrailStartColorUsePlayerColor : info.ContrailEndColorUsePlayerColor,
|
||||
info.ContrailStartWidth,
|
||||
info.ContrailEndWidth ?? info.ContrailStartWidth,
|
||||
info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
|
||||
}
|
||||
|
||||
trailPalette = info.TrailPalette;
|
||||
if (info.TrailUsePlayerPalette)
|
||||
trailPalette += args.SourceActor.Owner.InternalName;
|
||||
|
||||
shadowColor = new float3(info.ShadowColor.R, info.ShadowColor.G, info.ShadowColor.B) / 255f;
|
||||
shadowAlpha = info.ShadowColor.A / 255f;
|
||||
}
|
||||
|
||||
static int LoopRadius(int speed, int rot)
|
||||
{
|
||||
// loopRadius in w-units = speed in w-units per tick / angular speed in radians per tick
|
||||
// angular speed in radians per tick = rot in facing units per tick * (pi radians / 128 facing units)
|
||||
// pi = 314 / 100
|
||||
// ==> loopRadius = (speed * 128 * 100) / (314 * rot)
|
||||
return speed * 6400 / (157 * rot);
|
||||
}
|
||||
|
||||
void DetermineLaunchSpeedAndAngleForIncline(int predClfDist, int diffClfMslHgt, int relTarHorDist,
|
||||
out int speed, out int vFacing)
|
||||
{
|
||||
speed = maxLaunchSpeed;
|
||||
|
||||
// Find smallest vertical facing, for which the missile will be able to climb terrAltDiff w-units
|
||||
// within hHeightChange w-units all the while ending the ascent with vertical facing 0
|
||||
vFacing = maxLaunchAngle.Angle >> 2;
|
||||
|
||||
// Compute minimum speed necessary to both be able to face directly upwards and have enough space
|
||||
// to hit the target without passing it by (and thus having to do horizontal loops)
|
||||
var minSpeed = (System.Math.Min(predClfDist * 1024 / (1024 - WAngle.FromFacing(vFacing).Sin()),
|
||||
(relTarHorDist + predClfDist) * 1024 / (2 * (2048 - WAngle.FromFacing(vFacing).Sin())))
|
||||
* info.VerticalRateOfTurn.Facing * 157 / 6400).Clamp(minLaunchSpeed, maxLaunchSpeed);
|
||||
|
||||
if ((sbyte)vFacing < 0)
|
||||
speed = minSpeed;
|
||||
else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt)
|
||||
&& !WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt))
|
||||
{
|
||||
// Find highest speed greater than the above minimum that allows the missile
|
||||
// to surmount the incline
|
||||
var vFac = vFacing;
|
||||
speed = BisectionSearch(minSpeed, maxLaunchSpeed, spd =>
|
||||
{
|
||||
var lpRds = LoopRadius(spd, info.VerticalRateOfTurn.Facing);
|
||||
return WillClimbWithinDistance(vFac, lpRds, predClfDist, diffClfMslHgt)
|
||||
|| WillClimbAroundInclineTop(vFac, lpRds, predClfDist, diffClfMslHgt);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find least vertical facing that will allow the missile to climb
|
||||
// terrAltDiff w-units within hHeightChange w-units
|
||||
// all the while ending the ascent with vertical facing 0
|
||||
vFacing = BisectionSearch(System.Math.Max((sbyte)(minLaunchAngle.Angle >> 2), (sbyte)0),
|
||||
(sbyte)(maxLaunchAngle.Angle >> 2),
|
||||
vFac => !WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Double check Launch parameter determination
|
||||
void DetermineLaunchSpeedAndAngle(World world, out int speed, out int vFacing)
|
||||
{
|
||||
speed = maxLaunchSpeed;
|
||||
loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing);
|
||||
|
||||
// Compute current distance from target position
|
||||
var tarDistVec = targetPosition + offset - pos;
|
||||
var relTarHorDist = tarDistVec.HorizontalLength;
|
||||
|
||||
var predClfHgt = 0;
|
||||
var predClfDist = 0;
|
||||
var lastHt = 0;
|
||||
|
||||
if (info.TerrainHeightAware)
|
||||
InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out _, out lastHt);
|
||||
|
||||
// Height difference between the incline height and missile height
|
||||
var diffClfMslHgt = predClfHgt - pos.Z;
|
||||
|
||||
// Incline coming up
|
||||
if (info.TerrainHeightAware && diffClfMslHgt >= 0 && predClfDist > 0)
|
||||
DetermineLaunchSpeedAndAngleForIncline(predClfDist, diffClfMslHgt, relTarHorDist, out speed, out vFacing);
|
||||
else if (lastHt != 0)
|
||||
{
|
||||
vFacing = System.Math.Max((sbyte)(minLaunchAngle.Angle >> 2), (sbyte)0);
|
||||
speed = maxLaunchSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set vertical facing so that the missile faces its target
|
||||
var vDist = new WVec(-tarDistVec.Z, -relTarHorDist, 0);
|
||||
vFacing = (sbyte)vDist.Yaw.Facing;
|
||||
|
||||
// Do not accept -1 as valid vertical facing since it is usually a numerical error
|
||||
// and will lead to premature descent and crashing into the ground
|
||||
if (vFacing == -1)
|
||||
vFacing = 0;
|
||||
|
||||
// Make sure the chosen vertical facing adheres to prescribed bounds
|
||||
vFacing = vFacing.Clamp((sbyte)(minLaunchAngle.Angle >> 2),
|
||||
(sbyte)(maxLaunchAngle.Angle >> 2));
|
||||
}
|
||||
}
|
||||
|
||||
// Will missile be able to climb terrAltDiff w-units within hHeightChange w-units
|
||||
// all the while ending the ascent with vertical facing 0
|
||||
// Calling this function only makes sense when vFacing is nonnegative
|
||||
static bool WillClimbWithinDistance(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt)
|
||||
{
|
||||
// Missile's horizontal distance from loop's center
|
||||
var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024;
|
||||
|
||||
// Missile's height below loop's top
|
||||
var missHgt = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024;
|
||||
|
||||
// Height that would be climbed without changing vertical facing
|
||||
// for a horizontal distance hHeightChange - missDist
|
||||
var hgtChg = (predClfDist - missDist) * WAngle.FromFacing(vFacing).Tan() / 1024;
|
||||
|
||||
// Check if total manoeuvre height enough to overcome the incline's height
|
||||
return hgtChg + missHgt >= diffClfMslHgt;
|
||||
}
|
||||
|
||||
// This function checks if the missile's vertical facing is
|
||||
// nonnegative, and the incline top's horizontal distance from the missile is
|
||||
// less than loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024
|
||||
static bool IsNearInclineTop(int vFacing, int loopRadius, int predClfDist)
|
||||
{
|
||||
return vFacing >= 0 && predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024;
|
||||
}
|
||||
|
||||
// Will missile climb around incline top if bringing vertical facing
|
||||
// down to zero on an arc of radius loopRadius
|
||||
// Calling this function only makes sense when IsNearInclineTop returns true
|
||||
static bool WillClimbAroundInclineTop(int vFacing, int loopRadius, int predClfDist, int diffClfMslHgt)
|
||||
{
|
||||
// Vector from missile's current position pointing to the loop's center
|
||||
var radius = new WVec(loopRadius, 0, 0)
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(System.Math.Max(0, 64 - vFacing))));
|
||||
|
||||
// Vector from loop's center to incline top + 64 hardcoded in height buffer zone
|
||||
var topVector = new WVec(predClfDist, diffClfMslHgt + 64, 0) - radius;
|
||||
|
||||
// Check if incline top inside of the vertical loop
|
||||
return topVector.Length <= loopRadius;
|
||||
}
|
||||
|
||||
static int BisectionSearch(int lowerBound, int upperBound, System.Func<int, bool> testCriterion)
|
||||
{
|
||||
// Assuming that there exists an integer N between lowerBound and upperBound
|
||||
// for which testCriterion returns true as well as all integers less than N,
|
||||
// and for which testCriterion returns false for all integers greater than N,
|
||||
// this function finds N.
|
||||
while (upperBound - lowerBound > 1)
|
||||
{
|
||||
var middle = (upperBound + lowerBound) / 2;
|
||||
|
||||
if (testCriterion(middle))
|
||||
lowerBound = middle;
|
||||
else
|
||||
upperBound = middle;
|
||||
}
|
||||
|
||||
return lowerBound;
|
||||
}
|
||||
|
||||
bool JammedBy(TraitPair<JamsMissiles> tp)
|
||||
{
|
||||
if ((tp.Actor.CenterPosition - pos).HorizontalLengthSquared > tp.Trait.Range.LengthSquared)
|
||||
return false;
|
||||
|
||||
if (!tp.Trait.DeflectionStances.HasRelationship(tp.Actor.Owner.RelationshipWith(args.SourceActor.Owner)))
|
||||
return false;
|
||||
|
||||
return tp.Actor.World.SharedRandom.Next(100) < tp.Trait.Chance;
|
||||
}
|
||||
|
||||
void ChangeSpeed(int sign = 1)
|
||||
{
|
||||
speed = (speed + sign * info.Acceleration.Length).Clamp(0, maxSpeed);
|
||||
|
||||
// Compute the vertical loop radius
|
||||
loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing);
|
||||
}
|
||||
|
||||
WVec FreefallTick()
|
||||
{
|
||||
// Compute the projectile's freefall displacement
|
||||
var move = velocity + gravity / 2;
|
||||
velocity += gravity;
|
||||
var velRatio = maxSpeed * 1024 / velocity.Length;
|
||||
if (velRatio < 1024)
|
||||
velocity = velocity * velRatio / 1024;
|
||||
|
||||
return move;
|
||||
}
|
||||
|
||||
// NOTE: It might be desirable to make lookahead more intelligent by outputting more information
|
||||
// than just the highest point in the lookahead distance
|
||||
void InclineLookahead(World world, int distCheck, out int predClfHgt, out int predClfDist, out int lastHtChg, out int lastHt)
|
||||
{
|
||||
predClfHgt = 0; // Highest probed terrain height
|
||||
predClfDist = 0; // Distance from highest point
|
||||
lastHtChg = 0; // Distance from last time the height changes
|
||||
lastHt = 0; // Height just before the last height change
|
||||
|
||||
// NOTE: Might be desired to unhardcode the lookahead step size
|
||||
const int StepSize = 32;
|
||||
var step = new WVec(0, -StepSize, 0)
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing))); // Step vector of length 128
|
||||
|
||||
// Probe terrain ahead of the missile
|
||||
// NOTE: Might be desired to unhardcode maximum lookahead distance
|
||||
var maxLookaheadDistance = loopRadius * 4;
|
||||
var posProbe = pos;
|
||||
var curDist = 0;
|
||||
var tickLimit = System.Math.Min(maxLookaheadDistance, distCheck) / StepSize;
|
||||
var prevHt = 0;
|
||||
|
||||
// TODO: Make sure cell on map!!!
|
||||
for (var tick = 0; tick <= tickLimit; tick++)
|
||||
{
|
||||
posProbe += step;
|
||||
if (!world.Map.Contains(world.Map.CellContaining(posProbe)))
|
||||
break;
|
||||
|
||||
var ht = world.Map.Height[world.Map.CellContaining(posProbe)] * 512;
|
||||
|
||||
curDist += StepSize;
|
||||
if (ht > predClfHgt)
|
||||
{
|
||||
predClfHgt = ht;
|
||||
predClfDist = curDist;
|
||||
}
|
||||
|
||||
if (prevHt != ht)
|
||||
{
|
||||
lastHtChg = curDist;
|
||||
lastHt = prevHt;
|
||||
prevHt = ht;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int IncreaseAltitude(int predClfDist, int diffClfMslHgt, int relTarHorDist, int vFacing)
|
||||
{
|
||||
var desiredVFacing = vFacing;
|
||||
|
||||
// If missile is below incline top height and facing downwards, bring back
|
||||
// its vertical facing above zero as soon as possible
|
||||
if ((sbyte)vFacing < 0)
|
||||
desiredVFacing = info.VerticalRateOfTurn.Facing;
|
||||
|
||||
// Missile will climb around incline top if bringing vertical facing
|
||||
// down to zero on an arc of radius loopRadius
|
||||
else if (IsNearInclineTop(vFacing, loopRadius, predClfDist)
|
||||
&& WillClimbAroundInclineTop(vFacing, loopRadius, predClfDist, diffClfMslHgt))
|
||||
desiredVFacing = 0;
|
||||
|
||||
// Missile will not climb terrAltDiff w-units within hHeightChange w-units
|
||||
// all the while ending the ascent with vertical facing 0
|
||||
else if (!WillClimbWithinDistance(vFacing, loopRadius, predClfDist, diffClfMslHgt))
|
||||
|
||||
// Find smallest vertical facing, attainable in the next tick,
|
||||
// for which the missile will be able to climb terrAltDiff w-units
|
||||
// within hHeightChange w-units all the while ending the ascent
|
||||
// with vertical facing 0
|
||||
for (var vFac = System.Math.Min(vFacing + info.VerticalRateOfTurn.Facing - 1, 63); vFac >= vFacing; vFac--)
|
||||
if (!WillClimbWithinDistance(vFac, loopRadius, predClfDist, diffClfMslHgt)
|
||||
&& !(predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFac).Sin()) / 1024
|
||||
&& WillClimbAroundInclineTop(vFac, loopRadius, predClfDist, diffClfMslHgt)))
|
||||
{
|
||||
desiredVFacing = vFac + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
// Attained height after ascent as predicted from upper part of incline surmounting manoeuvre
|
||||
var predAttHght = loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024 - diffClfMslHgt;
|
||||
|
||||
// Should the missile be slowed down in order to make it more maneuverable
|
||||
var slowDown = info.Acceleration.Length != 0 // Possible to decelerate
|
||||
&& ((desiredVFacing != 0 // Lower part of incline surmounting manoeuvre
|
||||
|
||||
// Incline will be hit before vertical facing attains 64
|
||||
&& (predClfDist <= loopRadius * (1024 - WAngle.FromFacing(vFacing).Sin()) / 1024
|
||||
|
||||
// When evaluating this the incline will be *not* be hit before vertical facing attains 64
|
||||
// At current speed target too close to hit without passing it by
|
||||
|| relTarHorDist <= 2 * loopRadius * (2048 - WAngle.FromFacing(vFacing).Sin()) / 1024 - predClfDist))
|
||||
|
||||
|| (desiredVFacing == 0 // Upper part of incline surmounting manoeuvre
|
||||
&& relTarHorDist <= loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024
|
||||
+ Exts.ISqrt(predAttHght * (2 * loopRadius - predAttHght)))); // Target too close to hit at current speed
|
||||
|
||||
if (slowDown)
|
||||
ChangeSpeed(-1);
|
||||
|
||||
return desiredVFacing;
|
||||
}
|
||||
|
||||
int HomingInnerTick(int predClfDist, int diffClfMslHgt, int relTarHorDist, int lastHtChg, int lastHt,
|
||||
int relTarHgt, int vFacing, bool targetPassedBy)
|
||||
{
|
||||
int desiredVFacing;
|
||||
|
||||
// Incline coming up -> attempt to reach the incline so that after predClfDist
|
||||
// the height above the terrain is positive but as close to 0 as possible
|
||||
// Also, never change horizontal facing and never travel backwards
|
||||
// Possible techniques to avoid close cliffs are deceleration, turning
|
||||
// as sharply as possible to travel directly upwards and then returning
|
||||
// to zero vertical facing as low as possible while still not hitting the
|
||||
// high terrain. A last technique (and the preferred one, normally used when
|
||||
// the missile hasn't been fired near a cliff) is simply finding the smallest
|
||||
// vertical facing that allows for a smooth climb to the new terrain's height
|
||||
// and coming in at predClfDist at exactly zero vertical facing
|
||||
if (info.TerrainHeightAware && diffClfMslHgt >= 0 && !allowPassBy)
|
||||
desiredVFacing = IncreaseAltitude(predClfDist, diffClfMslHgt, relTarHorDist, vFacing);
|
||||
else if (relTarHorDist <= 3 * loopRadius || state == States.Hitting)
|
||||
{
|
||||
// No longer travel at cruise altitude
|
||||
state = States.Hitting;
|
||||
|
||||
if (lastHt >= targetPosition.Z)
|
||||
allowPassBy = true;
|
||||
|
||||
if (!allowPassBy && (lastHt < targetPosition.Z || targetPassedBy))
|
||||
{
|
||||
// Aim for the target
|
||||
var vDist = new WVec(-relTarHgt, -relTarHorDist, 0);
|
||||
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
|
||||
|
||||
// Do not accept -1 as valid vertical facing since it is usually a numerical error
|
||||
// and will lead to premature descent and crashing into the ground
|
||||
if (desiredVFacing == -1)
|
||||
desiredVFacing = 0;
|
||||
|
||||
// If the target has been passed by, limit the absolute value of
|
||||
// vertical facing by the maximum vertical rate of turn
|
||||
// Do this because the missile will be looping horizontally
|
||||
// and thus needs smaller vertical facings so as not
|
||||
// to hit the ground prematurely
|
||||
if (targetPassedBy)
|
||||
desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing);
|
||||
else if (lastHt == 0)
|
||||
{
|
||||
// Before the target is passed by, missile speed should be changed
|
||||
// Target's height above loop's center
|
||||
var tarHgt = (loopRadius * WAngle.FromFacing(vFacing).Cos() / 1024 - System.Math.Abs(relTarHgt)).Clamp(0, loopRadius);
|
||||
|
||||
// Target's horizontal distance from loop's center
|
||||
var tarDist = Exts.ISqrt(loopRadius * loopRadius - tarHgt * tarHgt);
|
||||
|
||||
// Missile's horizontal distance from loop's center
|
||||
var missDist = loopRadius * WAngle.FromFacing(vFacing).Sin() / 1024;
|
||||
|
||||
// If the current height does not permit the missile
|
||||
// to hit the target before passing it by, lower speed
|
||||
// Otherwise, increase speed
|
||||
if (relTarHorDist <= tarDist - System.Math.Sign(relTarHgt) * missDist)
|
||||
ChangeSpeed(-1);
|
||||
else
|
||||
ChangeSpeed();
|
||||
}
|
||||
}
|
||||
else if (allowPassBy || (lastHt != 0 && relTarHorDist - lastHtChg < loopRadius))
|
||||
{
|
||||
// Only activate this part if target too close to cliff
|
||||
allowPassBy = true;
|
||||
|
||||
// Vector from missile's current position pointing to the loop's center
|
||||
var radius = new WVec(loopRadius, 0, 0)
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFacing)));
|
||||
|
||||
// Vector from loop's center to incline top hardcoded in height buffer zone
|
||||
var edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius;
|
||||
|
||||
if (!targetPassedBy)
|
||||
{
|
||||
// Climb to critical height
|
||||
if (relTarHorDist > 2 * loopRadius)
|
||||
{
|
||||
// Target's distance from cliff
|
||||
var d1 = relTarHorDist - lastHtChg;
|
||||
if (d1 < 0)
|
||||
d1 = 0;
|
||||
if (d1 > 2 * loopRadius)
|
||||
return 0;
|
||||
|
||||
// Find critical height at which the missile must be once it is at one loopRadius
|
||||
// away from the target
|
||||
var h1 = loopRadius - Exts.ISqrt(d1 * (2 * loopRadius - d1)) - (pos.Z - lastHt);
|
||||
|
||||
if (h1 > loopRadius * (1024 - WAngle.FromFacing(vFacing).Cos()) / 1024)
|
||||
desiredVFacing = WAngle.ArcTan(Exts.ISqrt(h1 * (2 * loopRadius - h1)), loopRadius - h1).Angle >> 2;
|
||||
else
|
||||
desiredVFacing = 0;
|
||||
|
||||
// TODO: deceleration checks!!!
|
||||
}
|
||||
else
|
||||
{
|
||||
// Avoid the cliff edge
|
||||
if (info.TerrainHeightAware && edgeVector.Length > loopRadius && lastHt > targetPosition.Z)
|
||||
{
|
||||
int vFac;
|
||||
for (vFac = vFacing + 1; vFac <= vFacing + info.VerticalRateOfTurn.Facing - 1; vFac++)
|
||||
{
|
||||
// Vector from missile's current position pointing to the loop's center
|
||||
radius = new WVec(loopRadius, 0, 0)
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(64 - vFac)));
|
||||
|
||||
// Vector from loop's center to incline top + 64 hardcoded in height buffer zone
|
||||
edgeVector = new WVec(lastHtChg, lastHt - pos.Z, 0) - radius;
|
||||
if (edgeVector.Length <= loopRadius)
|
||||
break;
|
||||
}
|
||||
|
||||
desiredVFacing = vFac;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Aim for the target
|
||||
var vDist = new WVec(-relTarHgt, -relTarHorDist, 0);
|
||||
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
|
||||
if (desiredVFacing < 0 && info.VerticalRateOfTurn.Facing < (sbyte)vFacing)
|
||||
desiredVFacing = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Aim for the target
|
||||
var vDist = new WVec(-relTarHgt, relTarHorDist, 0);
|
||||
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
|
||||
if (desiredVFacing < 0 && info.VerticalRateOfTurn.Facing < (sbyte)vFacing)
|
||||
desiredVFacing = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Aim to attain cruise altitude as soon as possible while having the absolute value
|
||||
// of vertical facing bound by the maximum vertical rate of turn
|
||||
var vDist = new WVec(-diffClfMslHgt - info.CruiseAltitude.Length, -speed, 0);
|
||||
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
|
||||
|
||||
// If the missile is launched above CruiseAltitude, it has to descend instead of climbing
|
||||
if (-diffClfMslHgt > info.CruiseAltitude.Length)
|
||||
desiredVFacing = -desiredVFacing;
|
||||
|
||||
desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing);
|
||||
|
||||
ChangeSpeed();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Aim to attain cruise altitude as soon as possible while having the absolute value
|
||||
// of vertical facing bound by the maximum vertical rate of turn
|
||||
var vDist = new WVec(-diffClfMslHgt - info.CruiseAltitude.Length, -speed, 0);
|
||||
desiredVFacing = (sbyte)vDist.HorizontalLengthSquared != 0 ? vDist.Yaw.Facing : vFacing;
|
||||
|
||||
// If the missile is launched above CruiseAltitude, it has to descend instead of climbing
|
||||
if (-diffClfMslHgt > info.CruiseAltitude.Length)
|
||||
desiredVFacing = -desiredVFacing;
|
||||
|
||||
desiredVFacing = desiredVFacing.Clamp(-info.VerticalRateOfTurn.Facing, info.VerticalRateOfTurn.Facing);
|
||||
|
||||
ChangeSpeed();
|
||||
}
|
||||
|
||||
return desiredVFacing;
|
||||
}
|
||||
|
||||
WVec HomingTick(World world, in WVec tarDistVec, int relTarHorDist)
|
||||
{
|
||||
var predClfHgt = 0;
|
||||
var predClfDist = 0;
|
||||
var lastHtChg = 0;
|
||||
var lastHt = 0;
|
||||
|
||||
if (info.TerrainHeightAware)
|
||||
InclineLookahead(world, relTarHorDist, out predClfHgt, out predClfDist, out lastHtChg, out lastHt);
|
||||
|
||||
// Height difference between the incline height and missile height
|
||||
var diffClfMslHgt = predClfHgt - pos.Z;
|
||||
|
||||
// Get underestimate of distance from target in next tick
|
||||
var nxtRelTarHorDist = (relTarHorDist - speed - info.Acceleration.Length).Clamp(0, relTarHorDist);
|
||||
|
||||
// Target height relative to the missile
|
||||
var relTarHgt = tarDistVec.Z;
|
||||
|
||||
// Compute which direction the projectile should be facing
|
||||
var velVec = tarDistVec + predVel;
|
||||
var desiredHFacing = velVec.HorizontalLengthSquared != 0 ? velVec.Yaw.Facing : hFacing;
|
||||
|
||||
var delta = Util.NormalizeFacing(hFacing - desiredHFacing);
|
||||
if (allowPassBy && delta > 64 && delta < 192)
|
||||
{
|
||||
desiredHFacing = (desiredHFacing + 128) & 0xFF;
|
||||
targetPassedBy = true;
|
||||
}
|
||||
else
|
||||
targetPassedBy = false;
|
||||
|
||||
var desiredVFacing = HomingInnerTick(predClfDist, diffClfMslHgt, relTarHorDist, lastHtChg, lastHt,
|
||||
relTarHgt, vFacing, targetPassedBy);
|
||||
|
||||
// The target has been passed by
|
||||
if (tarDistVec.HorizontalLength < speed * WAngle.FromFacing(vFacing).Cos() / 1024)
|
||||
targetPassedBy = true;
|
||||
|
||||
// Check whether the homing mechanism is jammed
|
||||
var jammed = info.Jammable && world.ActorsWithTrait<JamsMissiles>().Any(JammedBy);
|
||||
if (jammed)
|
||||
{
|
||||
desiredHFacing = hFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1);
|
||||
desiredVFacing = vFacing + world.SharedRandom.Next(-info.JammedDiversionRange, info.JammedDiversionRange + 1);
|
||||
}
|
||||
else if (!args.GuidedTarget.IsValidFor(args.SourceActor))
|
||||
desiredHFacing = hFacing;
|
||||
|
||||
// Compute new direction the projectile will be facing
|
||||
hFacing = Util.TickFacing(hFacing, desiredHFacing, info.HorizontalRateOfTurn.Facing);
|
||||
vFacing = Util.TickFacing(vFacing, desiredVFacing, info.VerticalRateOfTurn.Facing);
|
||||
|
||||
// Compute the projectile's guided displacement
|
||||
return new WVec(0, -1024 * speed, 0)
|
||||
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)))
|
||||
/ 1024;
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
ticks++;
|
||||
anim?.Tick();
|
||||
|
||||
// Switch from freefall mode to homing mode
|
||||
if (ticks == info.HomingActivationDelay + 1)
|
||||
{
|
||||
state = States.Homing;
|
||||
speed = velocity.Length;
|
||||
|
||||
// Compute the vertical loop radius
|
||||
loopRadius = LoopRadius(speed, info.VerticalRateOfTurn.Facing);
|
||||
}
|
||||
|
||||
// Switch from homing mode to freefall mode
|
||||
if (rangeLimit >= WDist.Zero && distanceCovered > rangeLimit)
|
||||
{
|
||||
state = States.Freefall;
|
||||
velocity = new WVec(0, -speed, 0)
|
||||
.Rotate(new WRot(WAngle.FromFacing(vFacing), WAngle.Zero, WAngle.Zero))
|
||||
.Rotate(new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(hFacing)));
|
||||
}
|
||||
|
||||
// Check if target position should be updated (actor visible & locked on)
|
||||
var newTarPos = targetPosition;
|
||||
if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn)
|
||||
newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source))
|
||||
+ new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude);
|
||||
|
||||
// Compute target's predicted velocity vector (assuming uniform circular motion)
|
||||
var yaw1 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing);
|
||||
tarVel = newTarPos - targetPosition;
|
||||
var yaw2 = tarVel.HorizontalLengthSquared != 0 ? tarVel.Yaw : WAngle.FromFacing(hFacing);
|
||||
predVel = tarVel.Rotate(WRot.FromYaw(yaw2 - yaw1));
|
||||
targetPosition = newTarPos;
|
||||
|
||||
// Compute current distance from target position
|
||||
var tarDistVec = targetPosition + offset - pos;
|
||||
var relTarDist = tarDistVec.Length;
|
||||
var relTarHorDist = tarDistVec.HorizontalLength;
|
||||
|
||||
WVec move;
|
||||
if (state == States.Freefall)
|
||||
move = FreefallTick();
|
||||
else
|
||||
move = HomingTick(world, tarDistVec, relTarHorDist);
|
||||
|
||||
renderFacing = new WVec(move.X, move.Y - move.Z, 0).Yaw;
|
||||
|
||||
// Move the missile
|
||||
var lastPos = pos;
|
||||
if (info.AllowSnapping && state != States.Freefall && relTarDist < move.Length)
|
||||
pos = targetPosition + offset;
|
||||
else
|
||||
pos += move;
|
||||
|
||||
// Check for walls or other blocking obstacles
|
||||
var shouldExplode = false;
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, lastPos, pos, info.Width, out var blockedPos))
|
||||
{
|
||||
pos = blockedPos;
|
||||
shouldExplode = true;
|
||||
}
|
||||
|
||||
// Create the sprite trail effect
|
||||
if (!string.IsNullOrEmpty(info.TrailImage) && --ticksToNextSmoke < 0 && (state != States.Freefall || info.TrailWhenDeactivated))
|
||||
{
|
||||
world.AddFrameEndTask(w => w.Add(new SpriteEffect(pos - 3 * move / 2, renderFacing, w,
|
||||
info.TrailImage, info.TrailSequences.Random(world.SharedRandom), trailPalette)));
|
||||
|
||||
ticksToNextSmoke = info.TrailInterval;
|
||||
}
|
||||
|
||||
if (info.ContrailLength > 0)
|
||||
contrail.Update(pos);
|
||||
|
||||
distanceCovered += new WDist(speed);
|
||||
var cell = world.Map.CellContaining(pos);
|
||||
var height = world.Map.DistanceAboveTerrain(pos);
|
||||
shouldExplode |= height.Length < 0 // Hit the ground
|
||||
|| relTarDist < info.CloseEnough.Length // Within range
|
||||
|| (info.ExplodeWhenEmpty && rangeLimit >= WDist.Zero && distanceCovered > rangeLimit) // Ran out of fuel
|
||||
|| !world.Map.Contains(cell) // This also avoids an IndexOutOfRangeException in GetTerrainInfo below.
|
||||
|| (!string.IsNullOrEmpty(info.BoundToTerrainType) && world.Map.GetTerrainInfo(cell).Type != info.BoundToTerrainType) // Hit incompatible terrain
|
||||
|| (height.Length < info.AirburstAltitude.Length && relTarHorDist < info.CloseEnough.Length); // Airburst
|
||||
|
||||
if (shouldExplode)
|
||||
Explode(world);
|
||||
}
|
||||
|
||||
void Explode(World world)
|
||||
{
|
||||
if (info.ContrailLength > 0)
|
||||
world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail)));
|
||||
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
|
||||
// Don't blow up in our launcher's face!
|
||||
if (ticks <= info.Arm)
|
||||
return;
|
||||
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, WAngle.FromFacing(vFacing), WAngle.FromFacing(hFacing)),
|
||||
ImpactPosition = pos,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromPos(pos), warheadArgs);
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (info.ContrailLength > 0)
|
||||
yield return contrail;
|
||||
|
||||
if (anim == null)
|
||||
yield break;
|
||||
|
||||
var world = args.SourceActor.World;
|
||||
if (!world.FogObscures(pos))
|
||||
{
|
||||
var paletteName = info.Palette;
|
||||
if (paletteName != null && info.IsPlayerPalette)
|
||||
paletteName += args.SourceActor.Owner.InternalName;
|
||||
|
||||
var palette = wr.Palette(paletteName);
|
||||
|
||||
if (info.Shadow)
|
||||
{
|
||||
var dat = world.Map.DistanceAboveTerrain(pos);
|
||||
var shadowPos = pos - new WVec(0, 0, dat.Length);
|
||||
foreach (var r in anim.Render(shadowPos, palette))
|
||||
yield return ((IModifyableRenderable)r)
|
||||
.WithTint(shadowColor, ((IModifyableRenderable)r).TintModifiers | TintModifiers.ReplaceColor)
|
||||
.WithAlpha(shadowAlpha);
|
||||
}
|
||||
|
||||
foreach (var r in anim.Render(pos, palette))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
173
OpenRA.Mods.Common/Projectiles/NukeLaunch.cs
Normal file
173
OpenRA.Mods.Common/Projectiles/NukeLaunch.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Effects
|
||||
{
|
||||
public class NukeLaunch : IProjectile, ISpatiallyPartitionable
|
||||
{
|
||||
readonly Player firedBy;
|
||||
readonly Animation anim;
|
||||
readonly WeaponInfo weapon;
|
||||
readonly string weaponPalette;
|
||||
readonly string upSequence;
|
||||
readonly string downSequence;
|
||||
|
||||
readonly WPos ascendSource;
|
||||
readonly WPos ascendTarget;
|
||||
readonly WPos descendSource;
|
||||
readonly WPos descendTarget;
|
||||
readonly WDist detonationAltitude;
|
||||
readonly bool removeOnDetonation;
|
||||
readonly int impactDelay;
|
||||
readonly int turn;
|
||||
readonly string trailImage;
|
||||
readonly ImmutableArray<string> trailSequences;
|
||||
readonly string trailPalette;
|
||||
readonly int trailInterval;
|
||||
readonly int trailDelay;
|
||||
|
||||
WPos pos;
|
||||
int ticks, trailTicks;
|
||||
int launchDelay;
|
||||
bool isLaunched;
|
||||
bool detonated;
|
||||
|
||||
public NukeLaunch(Player firedBy, string image, WeaponInfo weapon, string weaponPalette, string upSequence, string downSequence,
|
||||
WPos launchPos, WPos targetPos, WDist detonationAltitude, bool removeOnDetonation, WDist velocity, int launchDelay, int impactDelay,
|
||||
bool skipAscent,
|
||||
string trailImage, ImmutableArray<string> trailSequences, string trailPalette, bool trailUsePlayerPalette, int trailDelay, int trailInterval)
|
||||
{
|
||||
this.firedBy = firedBy;
|
||||
this.weapon = weapon;
|
||||
this.weaponPalette = weaponPalette;
|
||||
this.upSequence = upSequence;
|
||||
this.downSequence = downSequence;
|
||||
this.launchDelay = launchDelay;
|
||||
this.impactDelay = impactDelay;
|
||||
turn = skipAscent ? 0 : impactDelay / 2;
|
||||
this.trailImage = trailImage;
|
||||
this.trailSequences = trailSequences;
|
||||
this.trailPalette = trailPalette;
|
||||
if (trailUsePlayerPalette)
|
||||
this.trailPalette += firedBy.InternalName;
|
||||
|
||||
this.trailInterval = trailInterval;
|
||||
this.trailDelay = trailDelay;
|
||||
trailTicks = trailDelay;
|
||||
|
||||
var offset = new WVec(WDist.Zero, WDist.Zero, velocity * (impactDelay - turn));
|
||||
ascendSource = launchPos;
|
||||
ascendTarget = launchPos + offset;
|
||||
descendSource = targetPos + offset;
|
||||
descendTarget = targetPos;
|
||||
this.detonationAltitude = detonationAltitude;
|
||||
this.removeOnDetonation = removeOnDetonation;
|
||||
|
||||
if (!string.IsNullOrEmpty(image))
|
||||
anim = new Animation(firedBy.World, image);
|
||||
|
||||
pos = skipAscent ? descendSource : ascendSource;
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
if (launchDelay-- > 0)
|
||||
return;
|
||||
|
||||
if (!isLaunched)
|
||||
{
|
||||
if (weapon.Report != null && weapon.Report.Length > 0)
|
||||
Game.Sound.Play(SoundType.World, weapon.Report, world, pos);
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
anim.PlayRepeating(upSequence);
|
||||
world.ScreenMap.Add(this, pos, anim.Image);
|
||||
}
|
||||
|
||||
isLaunched = true;
|
||||
}
|
||||
|
||||
if (anim != null)
|
||||
{
|
||||
anim.Tick();
|
||||
|
||||
if (ticks == turn)
|
||||
anim.PlayRepeating(downSequence);
|
||||
}
|
||||
|
||||
var isDescending = ticks >= turn;
|
||||
if (!isDescending)
|
||||
pos = WPos.LerpQuadratic(ascendSource, ascendTarget, WAngle.Zero, ticks, turn);
|
||||
else
|
||||
pos = WPos.LerpQuadratic(descendSource, descendTarget, WAngle.Zero, ticks - turn, impactDelay - turn);
|
||||
|
||||
if (!string.IsNullOrEmpty(trailImage) && --trailTicks < 0)
|
||||
{
|
||||
var trailPos = !isDescending ? WPos.LerpQuadratic(ascendSource, ascendTarget, WAngle.Zero, ticks - trailDelay, turn)
|
||||
: WPos.LerpQuadratic(descendSource, descendTarget, WAngle.Zero, ticks - turn - trailDelay, impactDelay - turn);
|
||||
|
||||
world.AddFrameEndTask(w => w.Add(new SpriteEffect(trailPos, w, trailImage, trailSequences.Random(world.SharedRandom),
|
||||
trailPalette)));
|
||||
|
||||
trailTicks = trailInterval;
|
||||
}
|
||||
|
||||
var dat = world.Map.DistanceAboveTerrain(pos);
|
||||
if (ticks == impactDelay || (isDescending && dat <= detonationAltitude))
|
||||
Explode(world, ticks == impactDelay || removeOnDetonation);
|
||||
|
||||
if (anim != null)
|
||||
world.ScreenMap.Update(this, pos, anim.Image);
|
||||
|
||||
ticks++;
|
||||
}
|
||||
|
||||
void Explode(World world, bool removeProjectile)
|
||||
{
|
||||
if (removeProjectile)
|
||||
world.AddFrameEndTask(w => { w.Remove(this); w.ScreenMap.Remove(this); });
|
||||
|
||||
if (detonated)
|
||||
return;
|
||||
|
||||
var target = Target.FromPos(pos);
|
||||
var warheadArgs = new WarheadArgs
|
||||
{
|
||||
Weapon = weapon,
|
||||
Source = target.CenterPosition,
|
||||
SourceActor = firedBy.PlayerActor,
|
||||
WeaponTarget = target
|
||||
};
|
||||
|
||||
weapon.Impact(target, warheadArgs);
|
||||
|
||||
detonated = true;
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (!isLaunched || anim == null)
|
||||
return [];
|
||||
|
||||
return anim.Render(pos, wr.Palette(weaponPalette));
|
||||
}
|
||||
|
||||
public float FractionComplete => ticks * 1f / impactDelay;
|
||||
}
|
||||
}
|
||||
257
OpenRA.Mods.Common/Projectiles/Railgun.cs
Normal file
257
OpenRA.Mods.Common/Projectiles/Railgun.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* 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.Collections.Generic;
|
||||
using OpenRA.GameRules;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.Common.Projectiles
|
||||
{
|
||||
[Desc("Laser effect with helix coiling around.")]
|
||||
public class RailgunInfo : IProjectileInfo
|
||||
{
|
||||
[Desc("Damage all units hit by the beam instead of just the target?")]
|
||||
public readonly bool DamageActorsInLine = false;
|
||||
|
||||
[Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")]
|
||||
public readonly WDist Inaccuracy = WDist.Zero;
|
||||
|
||||
[Desc("Controls the way inaccuracy is calculated. Possible values are " +
|
||||
"'Maximum' - scale from 0 to max with range, " +
|
||||
"'PerCellIncrement' - scale from 0 with range, " +
|
||||
"'Absolute' - use set value regardless of range.")]
|
||||
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
||||
|
||||
[Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")]
|
||||
public readonly bool Blockable = false;
|
||||
|
||||
[Desc("Duration of the beam and helix")]
|
||||
public readonly int Duration = 15;
|
||||
|
||||
[Desc("Equivalent to sequence ZOffset. Controls Z sorting.")]
|
||||
public readonly int ZOffset = 0;
|
||||
|
||||
[Desc("The width of the main trajectory. (\"beam\").")]
|
||||
public readonly WDist BeamWidth = new(86);
|
||||
|
||||
[Desc("The shape of the beam. Accepts values Cylindrical or Flat.")]
|
||||
public readonly BeamRenderableShape BeamShape = BeamRenderableShape.Cylindrical;
|
||||
|
||||
[Desc("Beam color in (A),R,G,B.")]
|
||||
public readonly Color BeamColor = Color.FromArgb(128, 255, 255, 255);
|
||||
|
||||
[Desc("When true, this will override BeamColor parameter and draw the laser with player color."
|
||||
+ " (Still uses BeamColor's alpha information)")]
|
||||
public readonly bool BeamPlayerColor = false;
|
||||
|
||||
[Desc("Beam alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")]
|
||||
public readonly int BeamAlphaDeltaPerTick = -8;
|
||||
|
||||
[Desc("Thickness of the helix")]
|
||||
public readonly WDist HelixThickness = new(32);
|
||||
|
||||
[Desc("The radius of the spiral effect. (WDist)")]
|
||||
public readonly WDist HelixRadius = new(64);
|
||||
|
||||
[Desc("Height of one complete helix turn, measured parallel to the axis of the helix (WDist)")]
|
||||
public readonly WDist HelixPitch = new(512);
|
||||
|
||||
[Desc("Helix radius gets + this value per tick during drawing")]
|
||||
public readonly int HelixRadiusDeltaPerTick = 8;
|
||||
|
||||
[Desc("Helix alpha gets + this value per tick during drawing; hence negative value makes it fade over time.")]
|
||||
public readonly int HelixAlphaDeltaPerTick = -8;
|
||||
|
||||
[Desc("Helix spins by this much over time each tick.")]
|
||||
public readonly WAngle HelixAngleDeltaPerTick = new(16);
|
||||
|
||||
[Desc("Draw each cycle of helix with this many quantization steps")]
|
||||
public readonly int QuantizationCount = 16;
|
||||
|
||||
[Desc("Helix color in (A),R,G,B.")]
|
||||
public readonly Color HelixColor = Color.FromArgb(128, 255, 255, 255);
|
||||
|
||||
[Desc("Draw helix in PlayerColor? Overrides RGB part of the HelixColor. (Still uses HelixColor's alpha information)")]
|
||||
public readonly bool HelixPlayerColor = false;
|
||||
|
||||
[Desc("Impact animation.")]
|
||||
public readonly string HitAnim = null;
|
||||
|
||||
[Desc("Sequence of impact animation to use.")]
|
||||
[SequenceReference(nameof(HitAnim), allowNullImage: true)]
|
||||
public readonly string HitAnimSequence = "idle";
|
||||
|
||||
[PaletteReference]
|
||||
public readonly string HitAnimPalette = "effect";
|
||||
|
||||
public IProjectile Create(ProjectileArgs args)
|
||||
{
|
||||
var bc = BeamPlayerColor ? Color.FromArgb(BeamColor.A, args.SourceActor.OwnerColor()) : BeamColor;
|
||||
var hc = HelixPlayerColor ? Color.FromArgb(HelixColor.A, args.SourceActor.OwnerColor()) : HelixColor;
|
||||
return new Railgun(args, this, bc, hc);
|
||||
}
|
||||
}
|
||||
|
||||
public class Railgun : IProjectile, ISync
|
||||
{
|
||||
readonly ProjectileArgs args;
|
||||
readonly RailgunInfo info;
|
||||
readonly Animation hitanim;
|
||||
public readonly Color BeamColor;
|
||||
public readonly Color HelixColor;
|
||||
|
||||
int ticks;
|
||||
bool animationComplete;
|
||||
|
||||
[VerifySync]
|
||||
WPos target;
|
||||
|
||||
// Computing these in Railgun instead of RailgunRenderable saves Info.Duration ticks of computation.
|
||||
// Fortunately, railguns don't track the target.
|
||||
public int CycleCount { get; private set; }
|
||||
public WVec SourceToTarget { get; private set; }
|
||||
public WVec ForwardStep { get; private set; }
|
||||
public WVec LeftVector { get; private set; }
|
||||
public WVec UpVector { get; private set; }
|
||||
public WAngle AngleStep { get; private set; }
|
||||
|
||||
public Railgun(ProjectileArgs args, RailgunInfo info, Color beamColor, Color helixColor)
|
||||
{
|
||||
this.args = args;
|
||||
this.info = info;
|
||||
target = args.PassiveTarget;
|
||||
|
||||
BeamColor = beamColor;
|
||||
HelixColor = helixColor;
|
||||
|
||||
if (info.Inaccuracy.Length > 0)
|
||||
{
|
||||
var maxInaccuracyOffset = Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args);
|
||||
target += WVec.FromPDF(args.SourceActor.World.SharedRandom, 2) * maxInaccuracyOffset / 1024;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.HitAnim))
|
||||
hitanim = new Animation(args.SourceActor.World, info.HitAnim);
|
||||
|
||||
CalculateVectors();
|
||||
}
|
||||
|
||||
void CalculateVectors()
|
||||
{
|
||||
// Check for blocking actors
|
||||
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(args.SourceActor.World, args.SourceActor.Owner, target, args.Source,
|
||||
info.BeamWidth, out var blockedPos))
|
||||
target = blockedPos;
|
||||
|
||||
// Note: WAngle.Sin(x) = 1024 * Math.Sin(2pi/1024 * x)
|
||||
AngleStep = new WAngle(1024 / info.QuantizationCount);
|
||||
|
||||
SourceToTarget = target - args.Source;
|
||||
|
||||
// Forward step, pointing from src to target.
|
||||
// QuantizationCont * forwardStep == One cycle of beam in src2target direction.
|
||||
ForwardStep = info.HelixPitch.Length * SourceToTarget / (info.QuantizationCount * SourceToTarget.Length);
|
||||
|
||||
// An easy vector to find which is perpendicular vector to forwardStep, with 0 Z component
|
||||
LeftVector = new WVec(ForwardStep.Y, -ForwardStep.X, 0);
|
||||
if (LeftVector.LengthSquared != 0)
|
||||
LeftVector = 1024 * LeftVector / LeftVector.Length;
|
||||
|
||||
// Vector that is pointing upwards from the ground
|
||||
UpVector = new WVec(
|
||||
-ForwardStep.X * ForwardStep.Z,
|
||||
-ForwardStep.Z * ForwardStep.Y,
|
||||
ForwardStep.X * ForwardStep.X + ForwardStep.Y * ForwardStep.Y);
|
||||
|
||||
if (UpVector.LengthSquared != 0)
|
||||
UpVector = 1024 * UpVector / UpVector.Length;
|
||||
|
||||
//// LeftVector and UpVector are unit vectors of size 1024.
|
||||
|
||||
CycleCount = SourceToTarget.Length / info.HelixPitch.Length;
|
||||
if (SourceToTarget.Length % info.HelixPitch.Length != 0)
|
||||
CycleCount++; // math.ceil, int version.
|
||||
|
||||
// Using ForwardStep * CycleCount, the helix and the main beam gets "out of sync"
|
||||
// if drawn from source to target. Instead, the main beam is drawn from source to end point of helix.
|
||||
// Trade-off between computation vs Railgun weapon range.
|
||||
// Modders must not have too large range for railgun weapons.
|
||||
SourceToTarget = info.QuantizationCount * CycleCount * ForwardStep;
|
||||
}
|
||||
|
||||
public void Tick(World world)
|
||||
{
|
||||
if (ticks == 0)
|
||||
{
|
||||
if (hitanim != null)
|
||||
hitanim.PlayThen(info.HitAnimSequence, () => animationComplete = true);
|
||||
else
|
||||
animationComplete = true;
|
||||
|
||||
if (!info.DamageActorsInLine)
|
||||
{
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(args.Source, target), args.Facing),
|
||||
ImpactPosition = target,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromPos(target), warheadArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
var actors = world.FindActorsOnLine(args.Source, target, info.BeamWidth);
|
||||
foreach (var a in actors)
|
||||
{
|
||||
var warheadArgs = new WarheadArgs(args)
|
||||
{
|
||||
ImpactOrientation = new WRot(WAngle.Zero, Util.GetVerticalAngle(args.Source, target), args.Facing),
|
||||
|
||||
// Calculating an impact position is bogus for line damage.
|
||||
// FindActorsOnLine guarantees that the beam touches the target's HitShape,
|
||||
// so we just assume a center hit to avoid bogus warhead recalculations.
|
||||
ImpactPosition = a.CenterPosition,
|
||||
};
|
||||
|
||||
args.Weapon.Impact(Target.FromActor(a), warheadArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hitanim?.Tick();
|
||||
|
||||
if (ticks++ > info.Duration && animationComplete)
|
||||
world.AddFrameEndTask(w => w.Remove(this));
|
||||
}
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (wr.World.FogObscures(target) &&
|
||||
wr.World.FogObscures(args.Source))
|
||||
yield break;
|
||||
|
||||
if (ticks < info.Duration)
|
||||
{
|
||||
yield return new RailgunHelixRenderable(args.Source, info.ZOffset, this, info, ticks);
|
||||
yield return new BeamRenderable(args.Source, info.ZOffset, SourceToTarget, info.BeamShape, info.BeamWidth,
|
||||
Color.FromArgb(BeamColor.A + info.BeamAlphaDeltaPerTick * ticks, BeamColor));
|
||||
}
|
||||
|
||||
if (hitanim != null)
|
||||
foreach (var r in hitanim.Render(target, wr.Palette(info.HitAnimPalette)))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user