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  //end hit first (which sets the weapon to the "held" state, with disabled physics and no special collision detection)
178  EndHit();
179  //ensure the physics body is enabled
180  item.body.PhysEnabled = true;
181  base.Drop(dropper, setTransform);
182  }
183 
184  public override void UpdateBroken(float deltaTime, Camera cam)
185  {
186  Update(deltaTime, cam);
187  }
188 
189  public override void Update(float deltaTime, Camera cam)
190  {
191  if (!item.body.Enabled)
192  {
193  impactQueue.Clear();
194  return;
195  }
196  if (picker == null || !picker.HeldItems.Contains(item))
197  {
198  impactQueue.Clear();
199  IsActive = false;
200  }
201  while (impactQueue.Count > 0)
202  {
203  var impact = impactQueue.Dequeue();
204  HandleImpact(impact);
205  }
206  //in case handling the impact does something to the picker
207  if (picker == null) { return; }
208  reloadTimer -= deltaTime;
209  if (reloadTimer < 0)
210  {
211  reloadTimer = 0;
212  }
213  if (!picker.IsKeyDown(InputType.Aim) && !hitting)
214  {
215  hitPos = 0.0f;
216  }
217  ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
219  {
220  item.FlipX(relativeToSub: false);
221  }
223  if (!hitting)
224  {
226  if (aim)
227  {
228  UpdateSwingPos(deltaTime, out Vector2 swingPos);
229  hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
230  ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos + swingPos, aim: false, hitPos, holdAngle + hitPos + aimAngle, aimMelee: true);
231  if (ac.InWater)
232  {
233  ac.LockFlipping();
234  }
235  }
236  else
237  {
238  hitPos = 0;
239  ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
240  }
241  }
242  else
243  {
244  // TODO: We might want to make this configurable
245  hitPos -= deltaTime * 15f;
246  if (Swing)
247  {
248  ac.HoldItem(deltaTime, item, handlePos, itemPos: SwingPos, aim: false, hitPos, holdAngle);
249  }
250  else
251  {
252  ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
253  }
254  if (hitPos < -MathHelper.Pi)
255  {
256  EndHit();
257  }
258  }
259  }
260 
264  private void ActivateNearbySleepingCharacters()
265  {
266  foreach (Character c in Character.CharacterList)
267  {
268  if (!c.Enabled || !c.AnimController.BodyInRest) { continue; }
269  //do a broad check first
270  if (Math.Abs(c.WorldPosition.X - item.WorldPosition.X) > 1000.0f) { continue; }
271  if (Math.Abs(c.WorldPosition.Y - item.WorldPosition.Y) > 1000.0f) { continue; }
272 
273  foreach (Limb limb in c.AnimController.Limbs)
274  {
275  float hitRange = 2.0f;
276  if (Vector2.DistanceSquared(limb.SimPosition, item.SimPosition) < hitRange * hitRange)
277  {
278  c.AnimController.BodyInRest = false;
279  break;
280  }
281  }
282  }
283  }
284 
285  private void SetUser(Character character)
286  {
287  if (User == character) { return; }
288  if (User != null && User.Removed) { User = null; }
289 
290  User = character;
291  }
292 
293  private void EndHit()
294  {
295  RestoreCollision();
296  hitting = false;
297  hitTargets.Clear();
298  hitPos = 0;
299  }
300 
301  private void RestoreCollision()
302  {
303  impactQueue.Clear();
304  item.body.FarseerBody.OnCollision -= OnCollision;
305  item.body.CollisionCategories = Physics.CollisionItem;
306  item.body.CollidesWith = Physics.DefaultItemCollidesWith;
307  item.body.FarseerBody.IsBullet = false;
308  item.body.PhysEnabled = false;
309  }
310 
311  private bool OnCollision(Fixture f1, Fixture f2, Contact contact)
312  {
313  if (User == null || User.Removed)
314  {
315  impactQueue.Enqueue(f2);
316  return true;
317  }
318 
319  contact.GetWorldManifold(out Vector2 normal, out var points);
320 
321  //ignore collision if there's a wall between the user and the contact point to prevent hitting through walls
323  points[0],
324  collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
325  allowInsideFixture: true,
326  customPredicate: (Fixture fixture) => { return fixture.CollidesWith.HasFlag(Physics.CollisionItem) && fixture.Body != f2.Body; }) != null)
327  {
328  return false;
329  }
330 
331  if (f2.Body.UserData is Limb targetLimb)
332  {
333  if (targetLimb.IsSevered || targetLimb.character == null || targetLimb.character == User) { return false; }
334  if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
335  var targetCharacter = targetLimb.character;
336  if (targetCharacter == picker) { return false; }
337  if (AllowHitMultiple)
338  {
339  if (hitTargets.Contains(targetCharacter)) { return false; }
340  }
341  else
342  {
343  if (hitTargets.Any(t => t is Character)) { return false; }
344  }
345  hitTargets.Add(targetCharacter);
346  }
347  else if (f2.Body.UserData is Character targetCharacter)
348  {
349  if (targetCharacter == picker || targetCharacter == User) { return false; }
350  if (targetCharacter.IgnoreMeleeWeapons) { return false; }
351  targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
352  if (AllowHitMultiple)
353  {
354  if (hitTargets.Contains(targetCharacter)) { return false; }
355  }
356  else
357  {
358  if (hitTargets.Any(t => t is Character)) { return false; }
359  }
360  hitTargets.Add(targetCharacter);
361  }
362  else if (!HitOnlyCharacters)
363  {
364  if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
365  {
366  if (AllowHitMultiple)
367  {
368  if (hitTargets.Contains(targetStructure)) { return true; }
369  }
370  else
371  {
372  if (hitTargets.Any(t => t is Structure)) { return true; }
373  }
374  hitTargets.Add(targetStructure);
375  }
376  else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem)
377  {
378  if (AllowHitMultiple)
379  {
380  if (hitTargets.Contains(targetItem)) { return true; }
381  }
382  else
383  {
384  if (hitTargets.Any(t => t is Item)) { return true; }
385  }
386  hitTargets.Add(targetItem);
387  }
388  else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
389  {
390  if (holdable.Item.GetRootInventoryOwner() == User) { return false; }
391  hitTargets.Add(holdable.Item);
392  }
393  }
394  else
395  {
396  return false;
397  }
398 
399  impactQueue.Enqueue(f2);
400 
401  return true;
402  }
403 
404  private System.Text.StringBuilder serverLogger;
405  private void HandleImpact(Fixture targetFixture)
406  {
407  var target = targetFixture.Body;
408  if (User == null || User.Removed || target == null)
409  {
410  RestoreCollision();
411  hitting = false;
412  User = null;
413  return;
414  }
415 
416  float damageMultiplier = 1 + User.GetStatValue(StatTypes.MeleeAttackMultiplier);
417  damageMultiplier *= 1.0f + item.GetQualityModifier(Quality.StatType.StrikingPowerMultiplier);
418 
419  Character user = User;
420  Limb targetLimb = target.UserData as Limb;
421  Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
422  Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
423  Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
424  Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
425  GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
426  if (Attack != null)
427  {
428  Attack.SetUser(user);
429  Attack.DamageMultiplier = damageMultiplier;
430  if (targetLimb != null)
431  {
432  if (targetLimb.character.Removed) { return; }
433  targetLimb.character.LastDamageSource = item;
434  Attack.DoDamageToLimb(user, targetLimb, item.WorldPosition, 1.0f);
435  }
436  else if (targetCharacter != null)
437  {
438  if (targetCharacter.Removed) { return; }
439  targetCharacter.LastDamageSource = item;
440  Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
441  }
442  else if (targetStructure != null)
443  {
444  if (targetStructure.Removed) { return; }
445  Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
446  }
447  else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
448  {
449  if (targetItem.Removed) { return; }
450  var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
451 #if CLIENT
452  if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
453  (user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
454  {
455  Character.Controlled.UpdateHUDProgressBar(targetItem,
456  targetItem.WorldPosition,
457  targetItem.Condition / targetItem.MaxCondition,
458  emptyColor: GUIStyle.HealthBarColorLow,
459  fullColor: GUIStyle.HealthBarColorHigh,
460  textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
461  }
462 #endif
463  }
464  else if (target.UserData is Holdable holdable && holdable.CanPush)
465  {
466  if (holdable.Item.Removed) { return; }
467  Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
468  RestoreCollision();
469  hitting = false;
470  User = null;
471  }
472  else
473  {
474  return;
475  }
476  }
477 
478  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
479 
480  ActionType conditionalActionType = ActionType.OnSuccess;
481  if (user != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(user))
482  {
483  conditionalActionType = ActionType.OnFailure;
484  }
485  if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
486  {
487  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
488  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
489  serverLogger ??= new System.Text.StringBuilder();
490  serverLogger.Clear();
491  serverLogger.Append($"{picker?.LogName} used {item.Name}");
492  if (item.ContainedItems != null && item.ContainedItems.Any())
493  {
494  serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})");
495  }
496  string targetName;
497  if (targetCharacter != null)
498  {
499  targetName = targetCharacter.LogName;
500  }
501  else if (targetItem != null)
502  {
503  targetName = targetItem.Name;
504  }
505  else if (targetStructure != null)
506  {
507  targetName = targetStructure.Name;
508  }
509  else
510  {
511  targetName = targetEntity.ToString();
512  }
513  serverLogger.Append($" on {targetName}.");
514 #if SERVER
515  Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack);
516 #endif
517  }
518  if (targetEntity != null)
519  {
520  ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
521  ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
522  }
523 
524  if (DeleteOnUse)
525  {
526  Entity.Spawner.AddItemToRemoveQueue(item);
527  }
528  }
529  }
530 }
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:189
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:184
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!