Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

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:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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 [];
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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;
}
}

View 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;
}
}
}