Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs
3 using FarseerPhysics;
4 using FarseerPhysics.Dynamics;
5 using Microsoft.Xna.Framework;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 
11 {
12  partial class RangedWeapon : ItemComponent
13  {
14  private float reload;
15  public float ReloadTimer { get; private set; }
16 
17  private Vector2 barrelPos;
18 
19  [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.")]
20  public string BarrelPos
21  {
22  get { return XMLExtensions.Vector2ToString(ConvertUnits.ToDisplayUnits(barrelPos)); }
23  set { barrelPos = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(value)); }
24  }
25 
26  [Serialize(1.0f, IsPropertySaveable.No, description: "How long the user has to wait before they can fire the weapon again (in seconds).")]
27  public float Reload
28  {
29  get { return reload; }
30  set { reload = Math.Max(value, 0.0f); }
31  }
32 
33  [Serialize(0f, IsPropertySaveable.No, description: "Weapons skill requirement to reload at normal speed.")]
35  {
36  get;
37  set;
38  }
39 
40  [Serialize(1.0f, IsPropertySaveable.No, description: "Reload time at 0 skill level. Reload time scales with skill level up to the Weapons skill requirement.")]
41  public float ReloadNoSkill
42  {
43  get;
44  set;
45  }
46 
47  [Serialize(false, IsPropertySaveable.No, description: "Tells the AI to hold the trigger down when it uses this weapon")]
48  public bool HoldTrigger
49  {
50  get;
51  set;
52  }
53 
54  [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
55  public int ProjectileCount
56  {
57  get;
58  set;
59  }
60 
61  [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).")]
62  public float Spread
63  {
64  get;
65  set;
66  }
67 
68  [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).")]
69  public float UnskilledSpread
70  {
71  get;
72  set;
73  }
74 
75  [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.")]
76  public float LaunchImpulse
77  {
78  get;
79  set;
80  }
81 
82  [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)]
83  public float Penetration { get; private set; }
84 
85  [Serialize(1f, IsPropertySaveable.Yes, description: "Weapon's damage modifier")]
86  public float WeaponDamageModifier
87  {
88  get;
89  private set;
90  }
91 
92  [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
93  public float MaxChargeTime
94  {
95  get;
96  private set;
97  }
98 
99  [Serialize(defaultValue: 1f, IsPropertySaveable.Yes, description: "Penalty multiplier to reload time when dual-wielding.")]
101  {
102  get;
103  private set;
104  }
105 
106  [Serialize(defaultValue: 0f, IsPropertySaveable.Yes, description: "Additive penalty to accuracy (spread angle) when dual-wielding.")]
108  {
109  get;
110  private set;
111  }
112 
113  private readonly IReadOnlySet<Identifier> suitableProjectiles;
114 
115 
116  private enum ChargingState
117  {
118  Inactive,
119  WindingUp,
120  WindingDown,
121  }
122  private ChargingState currentChargingState;
123 
124  public Vector2 TransformedBarrelPos
125  {
126  get
127  {
128  Matrix bodyTransform = Matrix.CreateRotationZ(item.body == null ? item.RotationRad : item.body.Rotation);
129  Vector2 flippedPos = barrelPos;
130  if (item.body != null && item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
131  return Vector2.Transform(flippedPos, bodyTransform) * item.Scale;
132  }
133  }
134 
135 
136  public Projectile LastProjectile { get; private set; }
137 
138  private float currentChargeTime;
139  private bool tryingToCharge;
140 
142  : base(item, element)
143  {
144  item.IsShootable = true;
145  if (element.Parent is { } parent)
146  {
147  item.RequireAimToUse = parent.GetAttributeBool(nameof(item.RequireAimToUse), true);
148  }
149 
150  characterUsable = true;
151  suitableProjectiles = element.GetAttributeIdentifierArray(nameof(suitableProjectiles), Array.Empty<Identifier>()).ToHashSet();
152  if (ReloadSkillRequirement > 0 && ReloadNoSkill <= reload)
153  {
154  DebugConsole.AddWarning($"Invalid XML at {item.Name}: ReloadNoSkill is lower or equal than it's reload skill, despite having ReloadSkillRequirement.",
156  }
157  InitProjSpecific(element);
158  }
159 
160  partial void InitProjSpecific(ContentXElement rangedWeaponElement);
161 
162  public override void Equip(Character character)
163  {
164  //clamp above 1 to prevent rapid-firing by swapping weapons
165  ReloadTimer = Math.Max(Math.Min(reload, 1.0f), ReloadTimer);
166  IsActive = true;
167  }
168 
169  public override void Update(float deltaTime, Camera cam)
170  {
171  ReloadTimer -= deltaTime;
172 
173  if (ReloadTimer < 0.0f)
174  {
175  ReloadTimer = 0.0f;
176  if (MaxChargeTime <= 0f)
177  {
178  IsActive = false;
179  return;
180  }
181  }
182 
183  float previousChargeTime = currentChargeTime;
184 
185  float chargeDeltaTime = tryingToCharge && ReloadTimer <= 0f ? deltaTime : -deltaTime;
186  currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
187 
188  tryingToCharge = false;
189 
190  if (currentChargeTime == 0f)
191  {
192  currentChargingState = ChargingState.Inactive;
193  }
194  else if (currentChargeTime < previousChargeTime)
195  {
196  currentChargingState = ChargingState.WindingDown;
197  }
198  else
199  {
200  // if we are charging up or at maxed charge, remain winding up
201  currentChargingState = ChargingState.WindingUp;
202  }
203 
204  UpdateProjSpecific(deltaTime);
205  }
206 
207  partial void UpdateProjSpecific(float deltaTime);
208 
209  private float GetSpread(Character user)
210  {
211  float degreeOfFailure = MathHelper.Clamp(1.0f - DegreeOfSuccess(user), 0.0f, 1.0f);
212  degreeOfFailure *= degreeOfFailure;
213  float spread = MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure) / (1f + user.GetStatValue(StatTypes.RangedSpreadReduction));
214  if (user.IsDualWieldingRangedWeapons())
215  {
216  spread += Math.Max(0f, ApplyDualWieldPenaltyReduction(user, DualWieldAccuracyPenalty, neutralValue: 0f));
217  }
218  return MathHelper.ToRadians(spread);
219  }
220 
228  private static float ApplyDualWieldPenaltyReduction(Character character, float originalPenalty, float neutralValue)
229  {
230  float statAdjustmentPrc = character.GetStatValue(StatTypes.DualWieldingPenaltyReduction);
231  statAdjustmentPrc = MathHelper.Clamp(statAdjustmentPrc, 0f, 1f);
232  float reducedPenaltyMultiplier = MathHelper.Lerp(originalPenalty, neutralValue, statAdjustmentPrc);
233  return reducedPenaltyMultiplier;
234  }
235 
236  private readonly List<Body> ignoredBodies = new List<Body>();
237  public override bool Use(float deltaTime, Character character = null)
238  {
239  tryingToCharge = true;
240  if (character == null || character.Removed) { return false; }
241  if ((item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) || ReloadTimer > 0.0f) { return false; }
242  if (currentChargeTime < MaxChargeTime) { return false; }
243 
244  IsActive = true;
245  float baseReloadTime = reload;
246  float weaponSkill = character.GetSkillLevel("weapons");
247 
248  bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement;
249  if (applyReloadFailure)
250  {
251  //Examples, assuming 40 weapon skill required: 1 - 40/40 = 0 ... 1 - 0/40 = 1 ... 1 - 20 / 40 = 0.5
252  float reloadFailure = MathHelper.Clamp(1 - (weaponSkill / ReloadSkillRequirement), 0, 1);
253  baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
254  }
255 
256  if (character.IsDualWieldingRangedWeapons())
257  {
258  baseReloadTime *= Math.Max(1f, ApplyDualWieldPenaltyReduction(character, DualWieldReloadTimePenaltyMultiplier, neutralValue: 1f));
259  }
260 
261  ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
262  ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
263 
264  currentChargeTime = 0f;
265 
266  var abilityRangedWeapon = new AbilityRangedWeapon(item);
267  character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon);
268 
269  if (item.AiTarget != null)
270  {
272  item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
273  }
274 
275  float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
276  degreeOfFailure *= degreeOfFailure;
277  if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
278  {
279  ApplyStatusEffects(ActionType.OnFailure, 1.0f, character);
280  }
281 
282  for (int i = 0; i < ProjectileCount; i++)
283  {
284  Projectile projectile = FindProjectile(triggerOnUseOnContainers: true);
285  if (projectile != null)
286  {
287  Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
288  float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
289  float spread = GetSpread(character) * projectile.GetSpreadFromPool();
290 
291  var lastProjectile = LastProjectile;
292  if (lastProjectile != projectile)
293  {
294  lastProjectile?.Item.GetComponent<Rope>()?.Snap();
295  }
296  float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
297  projectile.Launcher = item;
298 
299  ignoredBodies.Clear();
300  if (!projectile.DamageUser)
301  {
302  foreach (Limb l in character.AnimController.Limbs)
303  {
304  if (l.IsSevered) { continue; }
305  ignoredBodies.Add(l.body.FarseerBody);
306  }
307 
308  foreach (Item heldItem in character.HeldItems)
309  {
310  var holdable = heldItem.GetComponent<Holdable>();
311  if (holdable?.Pusher != null)
312  {
313  ignoredBodies.Add(holdable.Pusher.FarseerBody);
314  }
315  }
316  }
317  projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
318  projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
319  if (projectile.Item.body != null)
320  {
321  if (i == 0)
322  {
323  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);
324  }
325  projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
326  }
327  Item.RemoveContained(projectile.Item);
328  }
329  LastProjectile = projectile;
330  }
331 
332  LaunchProjSpecific();
333 
334  return true;
335  }
336 
337  public override bool SecondaryUse(float deltaTime, Character character = null)
338  {
339  return characterUsable || character == null;
340  }
341 
342  public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
343  {
344  foreach (ItemContainer container in item.GetComponents<ItemContainer>())
345  {
346  foreach (Item containedItem in container.Inventory.AllItemsMod)
347  {
348  if (containedItem == null) { continue; }
349  Projectile projectile = containedItem.GetComponent<Projectile>();
350  if (IsSuitableProjectile(projectile)) { return projectile; }
351 
352  //projectile not found, see if the contained item contains projectiles
353  var containedSubItems = containedItem.OwnInventory?.AllItemsMod;
354  if (containedSubItems == null) { continue; }
355  foreach (Item subItem in containedSubItems)
356  {
357  if (subItem == null) { continue; }
358  Projectile subProjectile = subItem.GetComponent<Projectile>();
359  //apply OnUse statuseffects to the container in case it has to react to it somehow
360  //(play a sound, spawn more projectiles, reduce condition...)
361  if (triggerOnUseOnContainers && subItem.Condition > 0.0f)
362  {
363  subItem.GetComponent<ItemContainer>()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f);
364  }
365  if (IsSuitableProjectile(subProjectile)) { return subProjectile; }
366  }
367  }
368  }
369  return null;
370  }
371 
372  private bool IsSuitableProjectile(Projectile projectile)
373  {
374  if (projectile?.Item == null) { return false; }
375  if (!suitableProjectiles.Any()) { return true; }
376  return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s));
377  }
378 
379  partial void LaunchProjSpecific();
380  }
382  {
384  {
385  Item = item;
386  }
387  public Item Item { get; set; }
388  }
389 }
virtual Vector2 AimSourceSimPos
float GetSkillLevel(string skillIdentifier)
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
float GetStatValue(StatTypes statType, bool includeSaved=true)
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys)
AITarget AiTarget
Definition: Entity.cs:55
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb limb=null, Entity useTarget=null, bool isNetworkEvent=false, Vector2? worldPosition=null)
Executes all StatusEffects of the specified type. Note that condition checks are ignored here: that s...
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
float GetQualityModifier(Quality.StatType statType)
The base class for components holding the different functionalities of the item
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List< Body > ignoredBodies, bool createNetworkEvent, float damageMultiplier=1f, float launchImpulseModifier=0f)
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180