Files
OpenRA/OpenRA.Game/GameRules/WeaponInfo.cs

278 lines
8.2 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2022 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.Effects;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.GameRules
{
public class ProjectileArgs
{
public WeaponInfo Weapon;
public int[] DamageModifiers;
public int[] InaccuracyModifiers;
public int[] RangeModifiers;
public WAngle Facing;
public Func<WAngle> CurrentMuzzleFacing;
public WPos Source;
public Func<WPos> CurrentSource;
public Actor SourceActor;
public WPos PassiveTarget;
public Target GuidedTarget;
}
public class WarheadArgs
{
public WeaponInfo Weapon;
public int[] DamageModifiers = Array.Empty<int>();
public WPos? Source;
public WRot ImpactOrientation;
public WPos ImpactPosition;
public Actor SourceActor;
public Target WeaponTarget;
public WarheadArgs(ProjectileArgs args)
{
Weapon = args.Weapon;
DamageModifiers = args.DamageModifiers;
ImpactPosition = args.PassiveTarget;
Source = args.Source;
SourceActor = args.SourceActor;
WeaponTarget = args.GuidedTarget;
}
// For places that only want to update some of the fields (usually DamageModifiers)
public WarheadArgs(WarheadArgs args)
{
Weapon = args.Weapon;
DamageModifiers = args.DamageModifiers;
Source = args.Source;
SourceActor = args.SourceActor;
WeaponTarget = args.WeaponTarget;
}
// Default empty constructor for callers that want to initialize fields themselves
public WarheadArgs() { }
}
public interface IProjectile : IEffect { }
public interface IProjectileInfo { IProjectile Create(ProjectileArgs args); }
public sealed class WeaponInfo
{
[Desc("The maximum range the weapon can fire.")]
public readonly WDist Range = WDist.Zero;
[Desc("First burst is aimed at this offset relative to target position.")]
public readonly WVec FirstBurstTargetOffset = WVec.Zero;
[Desc("Each burst after the first lands by this offset away from the previous burst.")]
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
[Desc("The sound played each time the weapon is fired.")]
public readonly string[] Report = null;
[Desc("Sound played only on first burst in a salvo.")]
public readonly string[] StartBurstReport = null;
[Desc("The sound played when the weapon is reloaded.")]
public readonly string[] AfterFireSound = null;
[Desc("Delay in ticks to play reloading sound.")]
public readonly int AfterFireSoundDelay = 0;
[Desc("Delay in ticks between reloading ammo magazines.")]
public readonly int ReloadDelay = 1;
[Desc("Number of shots in a single ammo magazine.")]
public readonly int Burst = 1;
[Desc("Can this weapon target attacker itself.")]
public readonly bool CanTargetSelf = false;
[Desc("What types of targets are affected.")]
public readonly BitSet<TargetableType> ValidTargets = new BitSet<TargetableType>("Ground", "Water");
[Desc("What types of targets are unaffected.", "Overrules ValidTargets.")]
public readonly BitSet<TargetableType> InvalidTargets;
static readonly BitSet<TargetableType> TargetTypeAir = new BitSet<TargetableType>("Air");
[Desc("If weapon is not directly targeting an actor and targeted position is above this altitude,",
"the weapon will ignore terrain target types and only check TargetTypeAir for validity.")]
public readonly WDist AirThreshold = new WDist(128);
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
"If multiple entries, their number needs to match Burst - 1.")]
public readonly int[] BurstDelays = { 5 };
[Desc("The minimum range the weapon can fire.")]
public readonly WDist MinRange = WDist.Zero;
[Desc("Does this weapon aim at the target's center regardless of other targetable offsets?")]
public readonly bool TargetActorCenter = false;
[FieldLoader.LoadUsing(nameof(LoadProjectile))]
public readonly IProjectileInfo Projectile;
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
public readonly List<IWarhead> Warheads = new List<IWarhead>();
/// <summary>
/// This constructor is used solely for documentation generation!
/// </summary>
public WeaponInfo() { }
public WeaponInfo(MiniYaml content)
{
// Resolve any weapon-level yaml inheritance or removals
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
content.Nodes = MiniYaml.Merge(new[] { content.Nodes });
FieldLoader.Load(this, content);
}
static object LoadProjectile(MiniYaml yaml)
{
if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
return null;
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
if (ret == null)
return null;
FieldLoader.Load(ret, proj);
return ret;
}
static object LoadWarheads(MiniYaml yaml)
{
var retList = new List<IWarhead>();
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
{
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
if (ret == null)
continue;
FieldLoader.Load(ret, node.Value);
retList.Add(ret);
}
return retList;
}
public bool IsValidTarget(BitSet<TargetableType> targetTypes)
{
return ValidTargets.Overlaps(targetTypes) && !InvalidTargets.Overlaps(targetTypes);
}
/// <summary>Checks if the weapon is valid against (can target) the target.</summary>
public bool IsValidAgainst(in Target target, World world, Actor firedBy)
{
if (target.Type == TargetType.Actor)
return IsValidAgainst(target.Actor, firedBy);
if (target.Type == TargetType.FrozenActor)
return IsValidAgainst(target.FrozenActor, firedBy);
if (target.Type == TargetType.Terrain)
{
var dat = world.Map.DistanceAboveTerrain(target.CenterPosition);
if (dat > AirThreshold)
return IsValidTarget(TargetTypeAir);
var cell = world.Map.CellContaining(target.CenterPosition);
if (!world.Map.Contains(cell))
return false;
var cellInfo = world.Map.GetTerrainInfo(cell);
if (!IsValidTarget(cellInfo.TargetTypes))
return false;
return true;
}
return false;
}
/// <summary>Checks if the weapon is valid against (can target) the actor.</summary>
public bool IsValidAgainst(Actor victim, Actor firedBy)
{
if (!CanTargetSelf && victim == firedBy)
return false;
var targetTypes = victim.GetEnabledTargetTypes();
if (!IsValidTarget(targetTypes))
return false;
// PERF: Avoid LINQ.
foreach (var warhead in Warheads)
if (warhead.IsValidAgainst(victim, firedBy))
return true;
return false;
}
/// <summary>Checks if the weapon is valid against (can target) the frozen actor.</summary>
public bool IsValidAgainst(FrozenActor victim, Actor firedBy)
{
if (!CanTargetSelf && victim.Actor == firedBy)
return false;
if (!IsValidTarget(victim.TargetTypes))
return false;
if (!Warheads.Any(w => w.IsValidAgainst(victim, firedBy)))
return false;
return true;
}
/// <summary>Applies all the weapon's warheads to the target.</summary>
public void Impact(in Target target, WarheadArgs args)
{
var world = args.SourceActor.World;
foreach (var warhead in Warheads)
{
if (warhead.Delay > 0)
{
// Lambdas can't use 'in' variables, so capture a copy for later
var delayedTarget = target;
world.AddFrameEndTask(w => w.Add(new DelayedImpact(warhead.Delay, warhead, delayedTarget, args)));
}
else
warhead.DoImpact(target, args);
}
}
/// <summary>Applies all the weapon's warheads to the target. Only use for projectile-less, special-case impacts.</summary>
public void Impact(in Target target, Actor firedBy)
{
// The impact will happen immediately at target.CenterPosition.
var args = new WarheadArgs
{
Weapon = this,
SourceActor = firedBy,
WeaponTarget = target
};
if (firedBy.OccupiesSpace != null)
args.Source = firedBy.CenterPosition;
Impact(target, args);
}
}
}