Allows to pass the horizontal facing/yaw and vertical angle/pitch of the carrier projectile to warheads for further use. Add ImpactPosition to WarheadArgs InflictDamage doesn't pass the impact pos directly, and the very point of WarheadArgs is to avoid adding more and more arguments to the warhead methods.
319 lines
10 KiB
C#
319 lines
10 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.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
|
|
{
|
|
public class BulletInfo : IProjectileInfo
|
|
{
|
|
[Desc("Projectile speed in WDist / tick, two values indicate variable velocity.")]
|
|
public readonly WDist[] Speed = { new WDist(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 and 'Absolute' - use set value regardless of range.")]
|
|
public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum;
|
|
|
|
[Desc("Image to display.")]
|
|
public readonly string Image = null;
|
|
|
|
[SequenceReference("Image")]
|
|
[Desc("Loop a randomly chosen sequence of Image from this list while this projectile is moving.")]
|
|
public readonly string[] Sequences = { "idle" };
|
|
|
|
[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;
|
|
|
|
[PaletteReference]
|
|
[Desc("Palette to use for this projectile's shadow if Shadow is true.")]
|
|
public readonly string ShadowPalette = "shadow";
|
|
|
|
[Desc("Trail animation.")]
|
|
public readonly string TrailImage = null;
|
|
|
|
[SequenceReference("TrailImage")]
|
|
[Desc("Loop a randomly chosen sequence of TrailImage from this list while this projectile is moving.")]
|
|
public readonly 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("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 WDist(1);
|
|
|
|
[Desc("Arc in WAngles, two values indicate variable arc.")]
|
|
public readonly 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("If projectile touches an actor with one of these stances during or after the first bounce, trigger explosion.")]
|
|
public readonly Stance ValidBounceBlockerStances = Stance.Enemy | Stance.Neutral;
|
|
|
|
[Desc("Altitude above terrain below which to explode. Zero effectively deactivates airburst.")]
|
|
public readonly WDist AirburstAltitude = WDist.Zero;
|
|
|
|
public readonly int ContrailLength = 0;
|
|
public readonly int ContrailZOffset = 2047;
|
|
public readonly Color ContrailColor = Color.White;
|
|
public readonly bool ContrailUsePlayerColor = false;
|
|
public readonly int ContrailDelay = 1;
|
|
public readonly WDist ContrailWidth = new WDist(64);
|
|
|
|
public IProjectile Create(ProjectileArgs args) { return new Bullet(this, args); }
|
|
}
|
|
|
|
public class Bullet : IProjectile, ISync
|
|
{
|
|
readonly BulletInfo info;
|
|
readonly ProjectileArgs args;
|
|
readonly Animation anim;
|
|
readonly WAngle facing;
|
|
readonly WAngle angle;
|
|
readonly WDist speed;
|
|
readonly string trailPalette;
|
|
|
|
ContrailRenderable contrail;
|
|
|
|
[Sync]
|
|
WPos pos, lastPos, target, source;
|
|
|
|
int length;
|
|
int ticks, smokeTicks;
|
|
int remainingBounces;
|
|
|
|
public Bullet(BulletInfo info, ProjectileArgs args)
|
|
{
|
|
this.info = info;
|
|
this.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))
|
|
{
|
|
anim = new Animation(world, info.Image, new Func<WAngle>(GetEffectiveFacing));
|
|
anim.PlayRepeating(info.Sequences.Random(world.SharedRandom));
|
|
}
|
|
|
|
if (info.ContrailLength > 0)
|
|
{
|
|
var color = info.ContrailUsePlayerColor ? ContrailRenderable.ChooseColor(args.SourceActor) : info.ContrailColor;
|
|
contrail = new ContrailRenderable(world, color, info.ContrailWidth, info.ContrailLength, info.ContrailDelay, info.ContrailZOffset);
|
|
}
|
|
|
|
trailPalette = info.TrailPalette;
|
|
if (info.TrailUsePlayerPalette)
|
|
trailPalette += args.SourceActor.Owner.InternalName;
|
|
|
|
smokeTicks = info.TrailDelay;
|
|
remainingBounces = info.BounceCount;
|
|
}
|
|
|
|
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 void Tick(World world)
|
|
{
|
|
if (anim != null)
|
|
anim.Tick();
|
|
|
|
lastPos = pos;
|
|
pos = WPos.LerpQuadratic(source, target, angle, ticks, length);
|
|
|
|
// Check for walls or other blocking obstacles
|
|
var shouldExplode = false;
|
|
WPos blockedPos;
|
|
if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, lastPos, pos, info.Width,
|
|
out blockedPos))
|
|
{
|
|
pos = blockedPos;
|
|
shouldExplode = 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)
|
|
{
|
|
shouldExplode |= AnyValidTargetsInRadius(world, pos, info.Width, args.SourceActor, 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
|
|
shouldExplode |= flightLengthReached && !shouldBounce;
|
|
|
|
// Driving into cell with higher height level
|
|
shouldExplode |= world.Map.DistanceAboveTerrain(pos).Length < 0;
|
|
|
|
// After first bounce, check for targets each tick
|
|
if (remainingBounces < info.BounceCount)
|
|
shouldExplode |= AnyValidTargetsInRadius(world, pos, info.Width, args.SourceActor, true);
|
|
|
|
if (shouldExplode)
|
|
Explode(world);
|
|
}
|
|
|
|
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
|
{
|
|
if (info.ContrailLength > 0)
|
|
yield return contrail;
|
|
|
|
if (anim == null || ticks >= length)
|
|
yield break;
|
|
|
|
var world = args.SourceActor.World;
|
|
if (!world.FogObscures(pos))
|
|
{
|
|
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, wr.Palette(info.ShadowPalette)))
|
|
yield return r;
|
|
}
|
|
|
|
var palette = wr.Palette(info.Palette + (info.IsPlayerPalette ? args.SourceActor.Owner.InternalName : ""));
|
|
foreach (var r in anim.Render(pos, palette))
|
|
yield return r;
|
|
}
|
|
}
|
|
|
|
void Explode(World world)
|
|
{
|
|
if (info.ContrailLength > 0)
|
|
world.AddFrameEndTask(w => w.Add(new ContrailFader(pos, contrail)));
|
|
|
|
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 (!info.ValidBounceBlockerStances.HasStance(victim.Owner.Stances[firedBy.Owner]))
|
|
continue;
|
|
|
|
// If the impact position is within any actor's HitShape, we have a direct hit
|
|
var activeShapes = victim.TraitsImplementing<HitShape>().Where(Exts.IsTraitEnabled);
|
|
if (activeShapes.Any(i => i.DistanceFromEdge(victim, pos).Length <= 0))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|