Server LuaCsForBarotrauma
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(Tags.WeaponsSkill);
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  //Note that we always snap the rope here, unlike when firing a rope from a turret.
295  //That's because handheld RangedWeapons have some special logic for handling the rope,
296  //which doesn't support multiple attached ropes (see Holdable.GetRope and the references to it)
297  lastProjectile?.Item.GetComponent<Rope>()?.Snap();
298  }
299  float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
300  projectile.Launcher = item;
301 
302  ignoredBodies.Clear();
303  if (!projectile.DamageUser)
304  {
305  foreach (Limb l in character.AnimController.Limbs)
306  {
307  if (l.IsSevered) { continue; }
308  ignoredBodies.Add(l.body.FarseerBody);
309  }
310 
311  foreach (Item heldItem in character.HeldItems)
312  {
313  var holdable = heldItem.GetComponent<Holdable>();
314  if (holdable?.Pusher != null)
315  {
316  ignoredBodies.Add(holdable.Pusher.FarseerBody);
317  }
318  }
319  }
320  projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
321  projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
322  if (projectile.Item.body != null)
323  {
324  if (i == 0)
325  {
326  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);
327  }
328  projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
329  }
330  Item.RemoveContained(projectile.Item);
331  }
332  LastProjectile = projectile;
333  }
334 
335  LaunchProjSpecific();
336 
337  return true;
338  }
339 
340  public override bool SecondaryUse(float deltaTime, Character character = null)
341  {
342  return characterUsable || character == null;
343  }
344 
345  public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
346  {
347  foreach (ItemContainer container in item.GetComponents<ItemContainer>())
348  {
349  foreach (Item containedItem in container.Inventory.AllItemsMod)
350  {
351  if (containedItem == null) { continue; }
352  Projectile projectile = containedItem.GetComponent<Projectile>();
353  if (IsSuitableProjectile(projectile)) { return projectile; }
354 
355  //projectile not found, see if the contained item contains projectiles
356  var containedSubItems = containedItem.OwnInventory?.AllItemsMod;
357  if (containedSubItems == null) { continue; }
358  foreach (Item subItem in containedSubItems)
359  {
360  if (subItem == null) { continue; }
361  Projectile subProjectile = subItem.GetComponent<Projectile>();
362  //apply OnUse statuseffects to the container in case it has to react to it somehow
363  //(play a sound, spawn more projectiles, reduce condition...)
364  if (triggerOnUseOnContainers && subItem.Condition > 0.0f)
365  {
366  subItem.GetComponent<ItemContainer>()?.Item.ApplyStatusEffects(ActionType.OnUse, 1.0f);
367  }
368  if (IsSuitableProjectile(subProjectile)) { return subProjectile; }
369  }
370  }
371  }
372  return null;
373  }
374 
375  private bool IsSuitableProjectile(Projectile projectile)
376  {
377  if (projectile?.Item == null) { return false; }
378  if (!suitableProjectiles.Any()) { return true; }
379  return suitableProjectiles.Any(s => projectile.Item.Prefab.Identifier == s || projectile.Item.HasTag(s));
380  }
381 
382  partial void LaunchProjSpecific();
383  }
385  {
387  {
388  Item = item;
389  }
390  public Item Item { get; set; }
391  }
392 }
virtual Vector2 AimSourceSimPos
void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
float GetStatValue(StatTypes statType, bool includeSaved=true)
float GetSkillLevel(Identifier skillIdentifier)
Get the character's current skill level, taking into account any temporary boosts from wearables and ...
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)
Projectile FindProjectile(bool triggerOnUseOnContainers=false)
RangedWeapon(Item item, ContentXElement element)
override void Update(float deltaTime, Camera cam)
override bool Use(float deltaTime, Character character=null)
override void Equip(Character character)
override bool SecondaryUse(float deltaTime, Character character=null)
bool? IsSevered
Definition: Limb.cs:351
PhysicsBody body
Definition: Limb.cs:217
ContentPackage? ContentPackage
Definition: Prefab.cs:37
readonly Identifier Identifier
Definition: Prefab.cs:34
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
AbilityEffectType
Definition: Enums.cs:140
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195