Client LuaCsForBarotrauma
MeleeWeapon.cs
1 using FarseerPhysics;
2 using FarseerPhysics.Dynamics;
3 using FarseerPhysics.Dynamics.Contacts;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Linq;
9 
11 {
13  {
14  private float hitPos;
15 
16  private bool hitting;
17 
18  private float range;
19  private float reload;
20 
21  private float reloadTimer;
22 
23  public Attack Attack { get; private set; }
24 
25  private readonly HashSet<Entity> hitTargets = new HashSet<Entity>();
26 
27  private readonly Queue<Fixture> impactQueue = new Queue<Fixture>();
28 
29  public Character User { get; private set; }
30 
31  [Serialize(0.0f, IsPropertySaveable.No, description: "An estimation of how close the item has to be to the target for it to hit. Used by AI characters to determine when they're close enough to hit a target.")]
32  public float Range
33  {
34  get { return ConvertUnits.ToDisplayUnits(range); }
35  set { range = ConvertUnits.ToSimUnits(value); }
36  }
37 
38  [Serialize(0.5f, IsPropertySaveable.No, description: "How long the user has to wait before they can hit with the weapon again (in seconds).")]
39  public float Reload
40  {
41  get { return reload; }
42  set { reload = Math.Max(0.0f, value); }
43  }
44 
45  [Serialize(false, IsPropertySaveable.No, description: "Can the weapon hit multiple targets per swing.")]
46  public bool AllowHitMultiple
47  {
48  get;
49  set;
50  }
51 
52  [Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")]
53  public bool HitOnlyCharacters
54  {
55  get;
56  set;
57  }
58 
60  public bool Swing { get; set; }
61 
62  [Editable, Serialize("2.0, 0.0", IsPropertySaveable.No)]
63  public Vector2 SwingPos { get; set; }
64 
65  [Editable, Serialize("3.0, -1.0", IsPropertySaveable.No)]
66  public Vector2 SwingForce { get; set; }
67 
68  public bool Hitting { get { return hitting; } }
69 
73  public readonly ImmutableHashSet<Identifier> PreferredContainedItems;
74 
76  : base(item, element)
77  {
78  foreach (var subElement in element.Elements())
79  {
80  if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
81  Attack = new Attack(subElement, item.Name + ", MeleeWeapon", item)
82  {
83  DamageRange = item.body == null ? 10.0f : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent())
84  };
85  }
86  item.IsShootable = true;
87  item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true);
88  PreferredContainedItems = element.GetAttributeIdentifierArray("preferredcontaineditems", Array.Empty<Identifier>()).ToImmutableHashSet();
89  }
90 
91  public override void Equip(Character character)
92  {
93  base.Equip(character);
94  //force a wait of at least 1 second when equipping the weapon, so you can't "rapid-fire" by swapping between weapons
95  const float forcedDelayOnEquip = 1.0f;
96  reloadTimer = Math.Max(Math.Min(reload, forcedDelayOnEquip), reloadTimer);
97  IsActive = true;
98  }
99 
100  public override bool Use(float deltaTime, Character character = null)
101  {
102  if (character == null || reloadTimer > 0.0f) { return false; }
103 #if CLIENT
104  if (!Item.RequireAimToUse && character.IsPlayer && (GUI.MouseOn != null || character.Inventory.visualSlots.Any(s => s.MouseOn()) || Inventory.DraggingItems.Any())) { return false; }
105 #endif
106  if (Item.RequireAimToUse && !character.IsKeyDown(InputType.Aim) || hitting) { return false; }
107 
108  //don't allow hitting if the character is already hitting with another weapon
109  foreach (Item heldItem in character.HeldItems)
110  {
111  var otherWeapon = heldItem.GetComponent<MeleeWeapon>();
112  if (otherWeapon == null) { continue; }
113  if (otherWeapon.hitting) { return false; }
114  }
115 
116  SetUser(character);
117 
118  if (Item.RequireAimToUse && hitPos < MathHelper.PiOver4) { return false; }
119 
120  ActivateNearbySleepingCharacters();
121  reloadTimer = reload;
122  reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
123  reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
124  character.AnimController.LockFlipping();
125 
126  item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
127  item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
128  item.body.FarseerBody.OnCollision += OnCollision;
129  item.body.FarseerBody.IsBullet = true;
130  item.body.PhysEnabled = true;
131 
132  if (Swing && !character.AnimController.InWater)
133  {
134  foreach (Limb l in character.AnimController.Limbs)
135  {
136  if (l.IsSevered) { continue; }
137  Vector2 force = new Vector2(character.AnimController.Dir * SwingForce.X, SwingForce.Y) * l.Mass;
138  switch (l.type)
139  {
140  case LimbType.Torso:
141  force *= 2;
142  break;
143  case LimbType.Legs:
144  case LimbType.LeftFoot:
145  case LimbType.LeftThigh:
146  case LimbType.LeftLeg:
147  case LimbType.RightFoot:
148  case LimbType.RightThigh:
149  case LimbType.RightLeg:
150  force = Vector2.Zero;
151  break;
152  }
153  l.body.ApplyLinearImpulse(force);
154  }
155  }
156 
157  hitting = true;
158  hitTargets.Clear();
159 
160  IsActive = true;
161 
162  if (item.AiTarget != null)
163  {
165  item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
166  }
167  return false;
168  }
169 
170  public override bool SecondaryUse(float deltaTime, Character character = null)
171  {
172  return characterUsable || character == null;
173  }
174 
175  public override void Drop(Character dropper, bool setTransform = true)
176  {
177  base.Drop(dropper, setTransform);
178  hitting = false;
179  hitPos = 0.0f;
180  }
181 
182  public override void UpdateBroken(float deltaTime, Camera cam)
183  {
184  Update(deltaTime, cam);
185  }
186 
187  public override void Update(float deltaTime, Camera cam)
188  {
189  if (!item.body.Enabled)
190  {
191  impactQueue.Clear();
192  return;
193  }
194  if (picker == null || !picker.HeldItems.Contains(item))
195  {
196  impactQueue.Clear();
197  IsActive = false;
198  }
199  while (impactQueue.Count > 0)
200  {
201  var impact = impactQueue.Dequeue();
202  HandleImpact(impact);
203  }
204  //in case handling the impact does something to the picker
205  if (picker == null) { return; }
206  reloadTimer -= deltaTime;
207  if (reloadTimer < 0)
208  {
209  reloadTimer = 0;
210  }
211  if (!picker.IsKeyDown(InputType.Aim) && !hitting)
212  {
213  hitPos = 0.0f;
214  }
215  ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
217  {
218  item.FlipX(relativeToSub: false);
219  }
221  if (!hitting)
222  {
224  if (aim)
225  {
226  UpdateSwingPos(deltaTime, out Vector2 swingPos);
227  hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
228  ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos + swingPos, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
229  if (ac.InWater)
230  {
231  ac.LockFlipping();
232  }
233  }
234  else
235  {
236  hitPos = 0;
237  ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
238  }
239  }
240  else
241  {
242  // TODO: We might want to make this configurable
243  hitPos -= deltaTime * 15f;
244  if (Swing)
245  {
246  ac.HoldItem(deltaTime, item, handlePos, itemPos: SwingPos, aim: false, hitPos, holdAngle);
247  }
248  else
249  {
250  ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
251  }
252  if (hitPos < -MathHelper.Pi)
253  {
254  RestoreCollision();
255  hitting = false;
256  hitTargets.Clear();
257  hitPos = 0;
258  }
259  }
260  }
261 
265  private void ActivateNearbySleepingCharacters()
266  {
267  foreach (Character c in Character.CharacterList)
268  {
269  if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
270  //do a broad check first
271  if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
272  if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
273 
274  foreach (Limb limb in c.AnimController.Limbs)
275  {
276  float hitRange = 2.0f;
277  if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < hitRange * hitRange)
278  {
279  c.AnimController.BodyInRest = false;
280  break;
281  }
282  }
283  }
284  }
285 
286  private void SetUser(Character character)
287  {
288  if (User == character) { return; }
289  if (User != null && User.Removed) { User = null; }
290 
291  User = character;
292  }
293 
294  private void RestoreCollision()
295  {
296  impactQueue.Clear();
297  item.body.FarseerBody.OnCollision -= OnCollision;
298  item.body.CollisionCategories = Physics.CollisionItem;
299  item.body.CollidesWith = Physics.DefaultItemCollidesWith;
300  item.body.FarseerBody.IsBullet = false;
301  item.body.PhysEnabled = false;
302  }
303 
304  private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
305  {
306  if (User == null || User.Removed)
307  {
308  impactQueue.Enqueue(f2);
309  return true;
310  }
311 
312  contact.GetWorldManifold(out Vector2 normal, out var points);
313 
314  //ignore collision if there's a wall between the user and the contact point to prevent hitting through walls
316  points[0],
317  collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
318  allowInsideFixture: true,
319  customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem) && fixture.Body != f2.Body; }) != null)
320  {
321  return false;
322  }
323 
324  if (f2.Body.UserData is Limb targetLimb)
325  {
326  if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
327  if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
328  var targetCharacter = targetLimb.character;
329  if (targetCharacter == picker) { return false; }
330  if (AllowHitMultiple)
331  {
332  if (hitTargets.Contains(targetCharacter)) { return false; }
333  }
334  else
335  {
336  if (hitTargets.Any(t => t is Character)) { return false; }
337  }
338  hitTargets.Add(targetCharacter);
339  }
340  else if (f2.Body.UserData is Character targetCharacter)
341  {
342  if (targetCharacter == picker || targetCharacter == User) { return false; }
343  if (targetCharacter.IgnoreMeleeWeapons) { return false; }
344  targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
345  if (AllowHitMultiple)
346  {
347  if (hitTargets.Contains(targetCharacter)) { return false; }
348  }
349  else
350  {
351  if (hitTargets.Any(t => t is Character)) { return false; }
352  }
353  hitTargets.Add(targetCharacter);
354  }
355  else if (!HitOnlyCharacters)
356  {
357  if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
358  {
359  if (AllowHitMultiple)
360  {
361  if (hitTargets.Contains(targetStructure)) { return true; }
362  }
363  else
364  {
365  if (hitTargets.Any(t => t is Structure)) { return true; }
366  }
367  hitTargets.Add(targetStructure);
368  }
369  else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem)
370  {
371  if (AllowHitMultiple)
372  {
373  if (hitTargets.Contains(targetItem)) { return true; }
374  }
375  else
376  {
377  if (hitTargets.Any(t => t is Item)) { return true; }
378  }
379  hitTargets.Add(targetItem);
380  }
381  else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
382  {
383  hitTargets.Add(holdable.Item);
384  }
385  }
386  else
387  {
388  return false;
389  }
390 
391  impactQueue.Enqueue(f2);
392 
393  return true;
394  }
395 
396  private System.Text.StringBuilder serverLogger;
397  private void HandleImpact(Fixture targetFixture)
398  {
399  var target = targetFixture.Body;
400  if (User == null || User.Removed || target == null)
401  {
402  RestoreCollision();
403  hitting = false;
404  User = null;
405  return;
406  }
407 
408  float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
409  damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier);
410 
411  Character user = User;
412  Limb targetLimb = target.UserData as Limb;
413  Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
414  Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
415  Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
416  Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
417  GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
418  if (Attack != null)
419  {
420  Attack.SetUser(user);
421  Attack.DamageMultiplier = damageMultiplier;
422  if (targetLimb != null)
423  {
424  if (targetLimb.character.Removed) { return; }
425  targetLimb.character.LastDamageSource = item;
426  Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
427  }
428  else if (targetCharacter != null)
429  {
430  if (targetCharacter.Removed) { return; }
431  targetCharacter.LastDamageSource = item;
432  Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
433  }
434  else if (targetStructure != null)
435  {
436  if (targetStructure.Removed) { return; }
437  Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
438  }
439  else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
440  {
441  if (targetItem.Removed) { return; }
442  var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
443 #if CLIENT
444  if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
445  (user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
446  {
447  Character.Controlled.UpdateHUDProgressBar(targetItem,
448  targetItem.WorldPosition,
449  targetItem.Condition / targetItem.MaxCondition,
450  emptyColor: GUIStyle.HealthBarColorLow,
451  fullColor: GUIStyle.HealthBarColorHigh,
452  textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
453  }
454 #endif
455  }
456  else if (target.UserData is Holdable holdable && holdable.CanPush)
457  {
458  if (holdable.Item.Removed) { return; }
459  Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
460  RestoreCollision();
461  hitting = false;
462  User = null;
463  }
464  else
465  {
466  return;
467  }
468  }
469 
470  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
471 
472  ActionType conditionalActionType = ActionType.OnSuccess;
473  if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user))
474  {
475  conditionalActionType = ActionType.OnFailure;
476  }
477  if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
478  {
479  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
480  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
481  serverLogger ??= new System.Text.StringBuilder();
482  serverLogger.Clear();
483  serverLogger.Append($"{picker?.LogName} used {item.Name}");
484  if (item.ContainedItems != null && item.ContainedItems.Any())
485  {
486  serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})");
487  }
488  string targetName;
489  if (targetCharacter != null)
490  {
491  targetName = targetCharacter.LogName;
492  }
493  else if (targetItem != null)
494  {
495  targetName = targetItem.Name;
496  }
497  else if (targetStructure != null)
498  {
499  targetName = targetStructure.Name;
500  }
501  else
502  {
503  targetName = targetEntity.ToString();
504  }
505  serverLogger.Append($" on {targetName}.");
506 #if SERVER
507  Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack);
508 #endif
509  }
510  if (targetEntity != null)
511  {
512  ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
513  ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
514  }
515 
516  if (DeleteOnUse)
517  {
518  Entity.Spawner.AddItemToRemoveQueue(item);
519  }
520  }
521  }
522 }
void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 itemPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle=0.0f, bool aimMelee=false, Vector2? targetPos=null)
virtual Vector2 AimSourceSimPos
void LockFlipping(float time=0.2f)
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
AttackResult DoDamageToLimb(Character attacker, Limb targetLimb, Vector2 worldPosition, float deltaTime, bool playSound=true, PhysicsBody sourceBody=null, Limb sourceLimb=null)
float DamageMultiplier
Used for multiplying all the damage.
AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound=true, PhysicsBody sourceBody=null, Limb sourceLimb=null)
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...
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
override void FlipX(bool relativeToSub)
Flip the entity horizontally
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
float GetQualityModifier(Quality.StatType statType)
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
override bool Use(float deltaTime, Character character=null)
Definition: MeleeWeapon.cs:100
readonly ImmutableHashSet< Identifier > PreferredContainedItems
Defines items that boost the weapon functionality, like battery cell for stun batons.
Definition: MeleeWeapon.cs:73
override void Equip(Character character)
Definition: MeleeWeapon.cs:91
MeleeWeapon(Item item, ContentXElement element)
Definition: MeleeWeapon.cs:75
override void Update(float deltaTime, Camera cam)
Definition: MeleeWeapon.cs:187
override bool SecondaryUse(float deltaTime, Character character=null)
Definition: MeleeWeapon.cs:170
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
Definition: MeleeWeapon.cs:175
override void UpdateBroken(float deltaTime, Camera cam)
Definition: MeleeWeapon.cs:182
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180