#region Copyright & License Information /* * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Drawing; using OpenRA.Effects; using OpenRA.GameRules; using OpenRA.Graphics; using OpenRA.Mods.Common.Effects; using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Traits; namespace OpenRA.Mods.Common.Projectiles { public class AreaBeamInfo : IProjectileInfo, IRulesetLoaded { [Desc("Projectile speed in WDist / tick, two values indicate a randomly picked velocity per beam.")] public readonly WDist[] Speed = { new WDist(128) }; [Desc("The maximum duration (in ticks) of each beam burst.")] public readonly int Duration = 10; [Desc("The number of ticks between the beam causing warhead impacts in its area of effect.")] public readonly int DamageInterval = 3; [Desc("The width of the beam.")] public readonly WDist Width = new WDist(512); [Desc("The shape of the beam. Accepts values Cylindrical or Flat.")] public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical; [Desc("How far beyond the target the projectile keeps on travelling.")] public readonly WDist BeyondTargetRange = new WDist(0); [Desc("Damage modifier applied at each range step.")] public readonly int[] Falloff = { 100, 100 }; [Desc("Ranges at which each Falloff step is defined.")] public readonly WDist[] Range = { WDist.Zero, new WDist(int.MaxValue) }; [Desc("Maximum offset at the maximum range.")] public readonly WDist Inaccuracy = WDist.Zero; [Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")] public readonly bool Blockable = false; [Desc("Does the beam follow the target.")] public readonly bool TrackTarget = false; [Desc("Should the beam be visually rendered? False = Beam is invisible.")] public readonly bool RenderBeam = true; [Desc("Equivalent to sequence ZOffset. Controls Z sorting.")] public readonly int ZOffset = 0; [Desc("Color of the beam.")] public readonly Color Color = Color.Red; [Desc("Beam color is the player's color.")] public readonly bool UsePlayerColor = false; [Desc("Scan radius for actors with projectile-blocking trait. If set to zero (default), it will automatically scale", "to the blocker with the largest health shape. Only set custom values if you know what you're doing.")] public WDist BlockerScanRadius = WDist.Zero; [Desc("Scan radius for actors damaged by beam. If set to zero (default), it will automatically scale to the largest health shape.", "Only set custom values if you know what you're doing.")] public WDist AreaVictimScanRadius = WDist.Zero; public IProjectile Create(ProjectileArgs args) { var c = UsePlayerColor ? args.SourceActor.Owner.Color.RGB : Color; return new AreaBeam(this, args, c); } public void RulesetLoaded(Ruleset rules, WeaponInfo wi) { if (BlockerScanRadius == WDist.Zero) BlockerScanRadius = Util.MinimumRequiredBlockerScanRadius(rules); if (AreaVictimScanRadius == WDist.Zero) AreaVictimScanRadius = Util.MinimumRequiredVictimScanRadius(rules); } } public class AreaBeam : IProjectile, ISync { readonly AreaBeamInfo info; readonly ProjectileArgs args; readonly AttackBase actorAttackBase; readonly Color color; readonly WDist speed; [Sync] WPos headPos; [Sync] WPos tailPos; [Sync] WPos target; int length; int towardsTargetFacing; int headTicks; int tailTicks; bool isHeadTravelling = true; bool isTailTravelling; bool continueTracking = true; bool IsBeamComplete { get { return !isHeadTravelling && headTicks >= length && !isTailTravelling && tailTicks >= length; } } public AreaBeam(AreaBeamInfo info, ProjectileArgs args, Color color) { this.info = info; this.args = args; this.color = color; actorAttackBase = args.SourceActor.Trait(); var world = args.SourceActor.World; if (info.Speed.Length > 1) speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length)); else speed = info.Speed[0]; // Both the head and tail start at the source actor, but initially only the head is travelling. headPos = args.Source; tailPos = headPos; target = args.PassiveTarget; if (info.Inaccuracy.Length > 0) { var inaccuracy = Util.ApplyPercentageModifiers(info.Inaccuracy.Length, args.InaccuracyModifiers); var maxOffset = inaccuracy * (target - headPos).Length / args.Weapon.Range.Length; target += WVec.FromPDF(world.SharedRandom, 2) * maxOffset / 1024; } towardsTargetFacing = (target - headPos).Yaw.Facing; // Update the target position with the range we shoot beyond the target by // I.e. we can deliberately overshoot, so aim for that position var dir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(towardsTargetFacing)); target += dir * info.BeyondTargetRange.Length / 1024; length = Math.Max((target - headPos).Length / speed.Length, 1); } void TrackTarget() { if (!continueTracking) return; if (args.GuidedTarget.IsValidFor(args.SourceActor)) { var guidedTargetPos = args.GuidedTarget.CenterPosition; var targetDistance = new WDist((guidedTargetPos - args.Source).Length); // Only continue tracking target if it's within weapon range + // BeyondTargetRange to avoid edge case stuttering (start firing and immediately stop again). if (targetDistance > args.Weapon.Range + info.BeyondTargetRange) StopTargeting(); else { target = guidedTargetPos; towardsTargetFacing = (target - args.Source).Yaw.Facing; // Update the target position with the range we shoot beyond the target by // I.e. we can deliberately overshoot, so aim for that position var dir = new WVec(0, -1024, 0).Rotate(WRot.FromFacing(towardsTargetFacing)); target += dir * info.BeyondTargetRange.Length / 1024; } } } void StopTargeting() { continueTracking = false; isTailTravelling = true; } public void Tick(World world) { if (info.TrackTarget) TrackTarget(); if (++headTicks >= length) { headPos = target; isHeadTravelling = false; } else if (isHeadTravelling) headPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, headTicks, length); if (tailTicks <= 0 && args.SourceActor.IsInWorld && !args.SourceActor.IsDead) { args.Source = args.CurrentSource(); tailPos = args.Source; } // Allow for leniency to avoid edge case stuttering (start firing and immediately stop again). var outOfWeaponRange = args.Weapon.Range + info.BeyondTargetRange < new WDist((args.PassiveTarget - args.Source).Length); // While the head is travelling, the tail must start to follow Duration ticks later. // Alternatively, also stop emitting the beam if source actor dies or is ordered to stop. if ((headTicks >= info.Duration && !isTailTravelling) || args.SourceActor.IsDead || !actorAttackBase.IsAttacking || outOfWeaponRange) StopTargeting(); if (isTailTravelling) { if (++tailTicks >= length) { tailPos = target; isTailTravelling = false; } else tailPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, tailTicks, length); } // Check for blocking actors WPos blockedPos; if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, tailPos, headPos, info.Width, info.BlockerScanRadius, out blockedPos)) { headPos = blockedPos; target = headPos; length = Math.Min(headTicks, length); } // Damage is applied to intersected actors every DamageInterval ticks if (headTicks % info.DamageInterval == 0) { var actors = world.FindActorsOnLine(tailPos, headPos, info.Width, info.AreaVictimScanRadius); foreach (var a in actors) { var adjustedModifiers = args.DamageModifiers.Append(GetFalloff((args.Source - a.CenterPosition).Length)); args.Weapon.Impact(Target.FromActor(a), args.SourceActor, adjustedModifiers); } } if (IsBeamComplete) world.AddFrameEndTask(w => w.Remove(this)); } public IEnumerable Render(WorldRenderer wr) { if (!IsBeamComplete && info.RenderBeam && !(wr.World.FogObscures(tailPos) && wr.World.FogObscures(headPos))) { var beamRender = new BeamRenderable(headPos, info.ZOffset, tailPos - headPos, info.Shape, info.Width, color); return new[] { (IRenderable)beamRender }; } return SpriteRenderable.None; } int GetFalloff(int distance) { var inner = info.Range[0].Length; for (var i = 1; i < info.Range.Length; i++) { var outer = info.Range[i].Length; if (outer > distance) return int2.Lerp(info.Falloff[i - 1], info.Falloff[i], distance - inner, outer - inner); inner = outer; } return 0; } } }