390 lines
16 KiB
C#
390 lines
16 KiB
C#
using Barotrauma.Abilities;
|
|
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using FarseerPhysics.Dynamics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class RangedWeapon : ItemComponent
|
|
{
|
|
private float reload;
|
|
public float ReloadTimer { get; private set; }
|
|
|
|
private Vector2 barrelPos;
|
|
|
|
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position of the barrel as an offset from the item's center (in pixels). Determines where the projectiles spawn.")]
|
|
public string BarrelPos
|
|
{
|
|
get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); }
|
|
set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); }
|
|
}
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.No, description: "How long the user has to wait before they can fire the weapon again (in seconds).")]
|
|
public float Reload
|
|
{
|
|
get { return reload; }
|
|
set { reload = Math.Max(value, 0.0f); }
|
|
}
|
|
|
|
[Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")]
|
|
public float ReloadSkillRequirement
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")]
|
|
public float ReloadNoSkill
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")]
|
|
public bool HoldTrigger
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
|
|
public int ProjectileCount
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")]
|
|
public float Spread
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles when used by a character with insufficient skills to use the weapon (in degrees).")]
|
|
public float UnskilledSpread
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched). Sum of weapon + projectile.")]
|
|
public float LaunchImpulse
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Percentage of damage mitigation ignored when hitting armored body parts (deflecting limbs). Sum of weapon + projectile."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1f)]
|
|
public float Penetration { get; private set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")]
|
|
public float WeaponDamageModifier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
|
|
public float MaxChargeTime
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(defaultValue: 1f, IsPropertySaveable.Yes, description: "Penalty multiplier to reload time when dual-wielding.")]
|
|
public float DualWieldReloadTimePenaltyMultiplier
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
[Serialize(defaultValue: 0f, IsPropertySaveable.Yes, description: "Additive penalty to accuracy (spread angle) when dual-wielding.")]
|
|
public float DualWieldAccuracyPenalty
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
private readonly IReadOnlySet<Identifier> suitableProjectiles;
|
|
|
|
|
|
private enum ChargingState
|
|
{
|
|
Inactive,
|
|
WindingUp,
|
|
WindingDown,
|
|
}
|
|
private ChargingState currentChargingState;
|
|
|
|
public Vector2 TransformedBarrelPos
|
|
{
|
|
get
|
|
{
|
|
Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? item.RotationRad : item.body.Rotation);
|
|
Vector2 flippedPos = barrelPos;
|
|
if (item.body != null && item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
|
|
return Vector2.Transform(flippedPos, bodyTransform) * item.Scale;
|
|
}
|
|
}
|
|
|
|
|
|
public Projectile LastProjectile { get; private set; }
|
|
|
|
private float currentChargeTime;
|
|
private bool tryingToCharge;
|
|
|
|
public RangedWeapon(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
item.IsShootable = true;
|
|
if (element.Parent is { } parent)
|
|
{
|
|
item.RequireAimToUse = parent.GetAttributeBool(nameof(item.RequireAimToUse), true);
|
|
}
|
|
|
|
characterUsable = true;
|
|
suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty<Identifier>()).ToHashSet();
|
|
if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload)
|
|
{
|
|
DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.",
|
|
item.Prefab.ContentPackage);
|
|
}
|
|
InitProjSpecific(element);
|
|
}
|
|
|
|
partial void InitProjSpecific(ContentXElement rangedWeaponElement);
|
|
|
|
public override void Equip(Character character)
|
|
{
|
|
//clamp above 1 to prevent rapid-firing by swapping weapons
|
|
ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer);
|
|
IsActive = true;
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
ReloadTimer -= deltaTime;
|
|
|
|
if (ReloadTimer < 0.0f)
|
|
{
|
|
ReloadTimer = 0.0f;
|
|
if (MaxChargeTime <= 0f)
|
|
{
|
|
IsActive = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
float previousChargeTime = currentChargeTime;
|
|
|
|
float chargeDeltaTime = tryingToCharge && ReloadTimer <= 0f ? deltaTime : -deltaTime;
|
|
currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
|
|
|
|
tryingToCharge = false;
|
|
|
|
if (currentChargeTime == 0f)
|
|
{
|
|
currentChargingState = ChargingState.Inactive;
|
|
}
|
|
else if (currentChargeTime < previousChargeTime)
|
|
{
|
|
currentChargingState = ChargingState.WindingDown;
|
|
}
|
|
else
|
|
{
|
|
// if we are charging up or at maxed charge, remain winding up
|
|
currentChargingState = ChargingState.WindingUp;
|
|
}
|
|
|
|
UpdateProjSpecific(deltaTime);
|
|
}
|
|
|
|
partial void UpdateProjSpecific(float deltaTime);
|
|
|
|
private float GetSpread(Character user)
|
|
{
|
|
float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f);
|
|
degreeOfFailure *= degreeOfFailure;
|
|
float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction));
|
|
if (user.IsDualWieldingRangedWeapons())
|
|
{
|
|
spread += Math.Max(0f, ApplyDualWieldPenaltyReduction(user, DualWieldAccuracyPenalty, neutralValue: 0f));
|
|
}
|
|
return MathHelper.ToRadians(spread);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lerps between the original penalty and a neutral value, which should be 1 for multipliers and 0 for additive penalties.
|
|
/// </summary>
|
|
/// <param name="character">The character to get stat values from</param>
|
|
/// <param name="originalPenalty">The original penalty value</param>
|
|
/// <param name="neutralValue">Neutral value to lerp towards. Should be 1 for multipliers and 0 for additives.</param>
|
|
/// <returns></returns>
|
|
private static float ApplyDualWieldPenaltyReduction(Character character, float originalPenalty, float neutralValue)
|
|
{
|
|
float statAdjustmentPrc = character.GetStatValue(StatTypes.DualWieldingPenaltyReduction);
|
|
statAdjustmentPrc = MathHelper.Clamp(statAdjustmentPrc, 0f, 1f);
|
|
float reducedPenaltyMultiplier = MathHelper.Lerp(originalPenalty, neutralValue, statAdjustmentPrc);
|
|
return reducedPenaltyMultiplier;
|
|
}
|
|
|
|
private readonly List<Body> ignoredBodies = new List<Body>();
|
|
public override bool Use(float deltaTime, Character character = null)
|
|
{
|
|
tryingToCharge = true;
|
|
if (character == null || character.Removed) { return false; }
|
|
if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; }
|
|
if (currentChargeTime < MaxChargeTime) { return false; }
|
|
|
|
IsActive = true;
|
|
float baseReloadTime = reload;
|
|
float weaponSkill = character.GetSkillLevel("weapons");
|
|
|
|
bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement;
|
|
if (applyReloadFailure)
|
|
{
|
|
//Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5
|
|
float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1);
|
|
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
|
|
}
|
|
|
|
if (character.IsDualWieldingRangedWeapons())
|
|
{
|
|
baseReloadTime *= Math.Max(1f, ApplyDualWieldPenaltyReduction(character, DualWieldReloadTimePenaltyMultiplier, neutralValue: 1f));
|
|
}
|
|
|
|
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
|
|
ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
|
|
|
|
currentChargeTime = 0f;
|
|
|
|
var abilityRangedWeapon = new AbilityRangedWeapon(item);
|
|
character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon);
|
|
|
|
if (item.AiTarget != null)
|
|
{
|
|
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
|
|
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
|
|
}
|
|
|
|
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
|
|
degreeOfFailure *= degreeOfFailure;
|
|
if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
|
|
{
|
|
ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
|
|
}
|
|
|
|
for (int i = 0; i < ProjectileCount; i++)
|
|
{
|
|
Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
|
|
if (projectile != null)
|
|
{
|
|
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
|
|
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
|
|
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
|
|
|
|
var lastProjectile = LastProjectile;
|
|
if (lastProjectile != projectile)
|
|
{
|
|
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
|
|
}
|
|
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
|
|
projectile.Launcher = item;
|
|
|
|
ignoredBodies.Clear();
|
|
if (!projectile.DamageUser)
|
|
{
|
|
foreach (Limb l in character.AnimController.Limbs)
|
|
{
|
|
if (l.IsSevered) { continue; }
|
|
ignoredBodies.Add(l.body.FarseerBody);
|
|
}
|
|
|
|
foreach (Item heldItem in character.HeldItems)
|
|
{
|
|
var holdable = heldItem.GetComponent<Holdable>();
|
|
if (holdable?.Pusher != null)
|
|
{
|
|
ignoredBodies.Add(holdable.Pusher.FarseerBody);
|
|
}
|
|
}
|
|
}
|
|
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
|
|
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
|
|
if (projectile.Item.body != null)
|
|
{
|
|
if (i == 0)
|
|
{
|
|
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
|
|
}
|
|
Item.RemoveContained(projectile.Item);
|
|
}
|
|
LastProjectile = projectile;
|
|
}
|
|
|
|
LaunchProjSpecific();
|
|
|
|
return true;
|
|
}
|
|
|
|
public override bool SecondaryUse(float deltaTime, Character character = null)
|
|
{
|
|
return characterUsable || character == null;
|
|
}
|
|
|
|
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
|
|
{
|
|
foreach (ItemContainer container in item.GetComponents<ItemContainer>())
|
|
{
|
|
foreach (Item containedItem in container.Inventory.AllItemsMod)
|
|
{
|
|
if (containedItem == null) { continue; }
|
|
Projectile projectile = containedItem.GetComponent<Projectile>();
|
|
if (IsSuitableProjectile(projectile)) { return projectile; }
|
|
|
|
//projectile not found, see if the contained item contains projectiles
|
|
var containedSubItems = containedItem.OwnInventory?.AllItemsMod;
|
|
if (containedSubItems == null) { continue; }
|
|
foreach (Item subItem in containedSubItems)
|
|
{
|
|
if (subItem == null) { continue; }
|
|
Projectile subProjectile = subItem.GetComponent<Projectile>();
|
|
//apply OnUse statuseffects to the container in case it has to react to it somehow
|
|
//(play a sound, spawn more projectiles, reduce condition...)
|
|
if (triggerOnUseOnContainers && subItem.Condition > 0.0f)
|
|
{
|
|
subItem.GetComponent<ItemContainer>()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f);
|
|
}
|
|
if (IsSuitableProjectile(subProjectile)) { return subProjectile; }
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private bool IsSuitableProjectile(Projectile projectile)
|
|
{
|
|
if (projectile?.Item == null) { return false; }
|
|
if (!suitableProjectiles.Any()) { return true; }
|
|
return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s));
|
|
}
|
|
|
|
partial void LaunchProjSpecific();
|
|
}
|
|
class AbilityRangedWeapon : AbilityObject, IAbilityItem
|
|
{
|
|
public AbilityRangedWeapon(Item item)
|
|
{
|
|
Item = item;
|
|
}
|
|
public Item Item { get; set; }
|
|
}
|
|
}
|