Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Projectile.cs
2 using FarseerPhysics;
3 using FarseerPhysics.Dynamics;
4 using FarseerPhysics.Dynamics.Contacts;
5 using FarseerPhysics.Dynamics.Joints;
6 using Microsoft.Xna.Framework;
7 using System;
8 using System.Collections.Generic;
9 using System.Collections.Immutable;
10 using System.Linq;
11 using Voronoi2;
12 
14 {
16  {
17  private static readonly ImmutableArray<float> spreadPool;
18  static Projectile()
19  {
20  MTRandom random = new MTRandom(0);
21  spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
22  }
23 
24  public static byte SpreadCounter { get; private set; }
25 
26  public static void ResetSpreadCounter()
27  {
28  SpreadCounter = 0;
29  }
30 
31  struct HitscanResult
32  {
33  public Fixture Fixture;
34  public Vector2 Point;
35  public Vector2 Normal;
36  public float Fraction;
37  public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
38  {
39  Fixture = fixture;
40  Point = point;
41  Normal = normal;
42  Fraction = fraction;
43  }
44  }
45  struct Impact
46  {
47  public Fixture Fixture;
48  public Vector2 Normal;
49  public Vector2 LinearVelocity;
50 
51  public Impact(Fixture fixture, Vector2 normal, Vector2 velocity)
52  {
53  Fixture = fixture;
54  Normal = normal;
55  LinearVelocity = velocity;
56  }
57  }
58 
59  enum StickTargetType
60  {
61  Structure,
62  Limb,
63  Item,
64  Submarine,
65  LevelWall,
66  Unknown
67  }
68 
69  public const float WaterDragCoefficient = 0.1f;
70 
71  private readonly Queue<Impact> impactQueue = new Queue<Impact>();
72 
73  private bool removePending;
74 
75  private byte spreadIndex;
76 
77  //continuous collision detection is used while the projectile is moving faster than this
78  const float ContinuousCollisionThreshold = 5.0f;
79 
80  private Joint stickJoint;
81  private Vector2 jointAxis;
82 
83  public Attack Attack { get; private set; }
84 
85  private Vector2 launchPos;
87 
88  private readonly HashSet<Body> hits = new HashSet<Body>();
89 
90  public List<Body> IgnoredBodies;
91 
95  public Item Launcher;
96 
97  private Character stickTargetCharacter;
98 
99  private Character _user;
101  {
102  get { return _user; }
103  set
104  {
105  _user = value;
106  Attack?.SetUser(_user);
107  }
108  }
109 
110  public Character Attacker { get; set; }
111 
112  public IEnumerable<Body> Hits
113  {
114  get { return hits; }
115  }
116 
117  [Serialize(10.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
118  public float LaunchImpulse { get; set; }
119 
120  [Serialize(0.0f, IsPropertySaveable.No, description: "The random percentage modifier used to add variance to the launch impulse.")]
121  public float ImpulseSpread { get; set; }
122 
123  [Serialize(0.0f, IsPropertySaveable.No, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")]
124 
125  public float LaunchRotation
126  {
127  get { return MathHelper.ToDegrees(LaunchRotationRadians); }
128  set { LaunchRotationRadians = MathHelper.ToRadians(value); }
129  }
130 
132  {
133  get;
134  private set;
135  }
136 
137  [Serialize(false, IsPropertySaveable.No, description: "When set to true, the item can stick to any target it hits.")]
138  //backwards compatibility, can stick to anything
139  public bool DoesStick
140  {
141  get;
142  set;
143  }
144 
145  [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to characters.")]
146  public bool StickToCharacters
147  {
148  get;
149  set;
150  }
151 
152  [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to walls.")]
153  public bool StickToStructures
154  {
155  get;
156  set;
157  }
158 
159  [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to items.")]
160  public bool StickToItems
161  {
162  get;
163  set;
164  }
165 
166  [Serialize(false, IsPropertySaveable.No, description: "Can the projectile stick to doors. Caution: may cause issues.")]
167  public bool StickToDoors
168  {
169  get;
170  set;
171  }
172 
173  [Serialize(false, IsPropertySaveable.No, description: "Can the item stick even to deflective targets.")]
174  public bool StickToDeflective
175  {
176  get;
177  set;
178  }
179 
180  [Serialize(false, IsPropertySaveable.No, description: "")]
182  {
183  get;
184  set;
185  }
186 
187  [Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
188  "It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
189  public bool Hitscan
190  {
191  get;
192  set;
193  }
194 
195  [Serialize(1, IsPropertySaveable.No, description: "How many hitscans should be done when the projectile is launched. "
196  + "Multiple hitscans can be used to simulate weapons that fire multiple projectiles at the same time" +
197  " without having to actually use multiple projectile items, for example shotguns.")]
198  public int HitScanCount
199  {
200  get;
201  set;
202  }
203 
204  [Serialize(1, IsPropertySaveable.No, description: "How many targets the projectile can hit before it stops.")]
205  public int MaxTargetsToHit
206  {
207  get;
208  set;
209  }
210 
211  [Serialize(false, IsPropertySaveable.No, description: "Should the item be deleted when it hits something.")]
212  public bool RemoveOnHit
213  {
214  get;
215  set;
216  }
217 
218  [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the launch angle of the projectile (in degrees).")]
219  public float Spread
220  {
221  get;
222  set;
223  }
224 
225  [Serialize(false, IsPropertySaveable.No, description: "Override random spread with static spread; projectiles are launched with an equal amount of angle between them. Only applies when firing multiple projectiles.")]
226  public bool StaticSpread
227  {
228  get;
229  set;
230  }
231 
232  [Serialize(true, IsPropertySaveable.No)]
233  public bool FriendlyFire
234  {
235  get;
236  set;
237  }
238 
239  private float deactivationTimer;
240 
241  [Serialize(0f, IsPropertySaveable.No)]
242  public float DeactivationTime
243  {
244  get;
245  set;
246  }
247 
248  private float stickTimer;
249  [Serialize(0f, IsPropertySaveable.No)]
250  public float StickDuration
251  {
252  get;
253  set;
254  }
255 
256  [Serialize(-1f, IsPropertySaveable.No)]
257  public float MaxJointTranslation
258  {
259  get;
260  set;
261  }
262  private float maxJointTranslationInSimUnits = -1;
263 
264  [Serialize(true, IsPropertySaveable.No)]
265  public bool Prismatic
266  {
267  get;
268  set;
269  }
270 
271  [Serialize(false, IsPropertySaveable.No, description:"Enable only if you want to make the projectile ignore collisions with other projectiles when it's shot. Doesn't have any effect, if the item is not set to be damaged by projectiles.")]
273  {
274  get;
275  set;
276  }
277 
278  public Body StickTarget
279  {
280  get;
281  private set;
282  }
283 
284  [Serialize(false, IsPropertySaveable.No)]
285  public bool DamageDoors
286  {
287  get;
288  set;
289  }
290 
291  [Serialize(false, IsPropertySaveable.No, description: "Can the projectile hit the user? Should generally be disabled, unless the projectile is for example something like shrapnel launched by a projectile impact.")]
292  public bool DamageUser
293  {
294  get;
295  set;
296  }
297 
298  public bool IsStuckToTarget => StickTarget != null;
299 
300  private Category originalCollisionCategories;
301  private Category originalCollisionTargets;
302 
304  : base (item, element)
305  {
306  IgnoredBodies = new List<Body>();
307 
308  foreach (var subElement in element.Elements())
309  {
310  if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
311  Attack = new Attack(subElement, item.Name + ", Projectile", item);
312  }
313 
314  if (item.body == null)
315  {
316  DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!",
317  contentPackage: element.ContentPackage);
318  return;
319  }
320 
321  spreadIndex = SpreadCounter;
322  SpreadCounter++;
323 
324  InitProjSpecific(element);
325  }
326  partial void InitProjSpecific(ContentXElement element);
327 
328  public override void OnItemLoaded()
329  {
330  if (item.body == null) { return; }
331  if (Attack != null && Attack.DamageRange <= 0.0f)
332  {
333  switch (item.body.BodyShape)
334  {
335  case PhysicsBody.Shape.Circle:
337  break;
338  case PhysicsBody.Shape.Capsule:
340  break;
341  case PhysicsBody.Shape.Rectangle:
342  Attack.DamageRange = new Vector2(item.body.Width / 2.0f, item.body.Height / 2.0f).Length();
343  break;
344  }
345  Attack.DamageRange = ConvertUnits.ToDisplayUnits(Attack.DamageRange);
346  }
347  originalCollisionCategories = item.body.CollisionCategories;
348  originalCollisionTargets = item.body.CollidesWith;
349  }
350 
351  public float GetSpreadFromPool()
352  {
353  spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
354  return spreadPool[spreadIndex];
355  }
356 
357  private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
358  {
359  if (Item.body == null) { return; }
361  Item.SetTransform(simPosition, rotation);
362  if (Attack != null)
363  {
364  Attack.DamageMultiplier = damageMultiplier;
365  }
366  // Set user for hitscan projectiles to work properly.
367  User = user;
368  // Need to set null for non-characterusable items.
369  Use(character: null, launchImpulseModifier);
370  // Set user for normal projectiles to work properly.
371  User = user;
372  if (Item.Removed) { return; }
373  launchPos = simPosition;
375  //set the rotation of the projectile again because dropping the projectile resets the rotation
376  Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
377  if (DeactivationTime > 0)
378  {
379  deactivationTimer = DeactivationTime;
380  }
381  }
382 
383  public void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List<Body> ignoredBodies, bool createNetworkEvent, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
384  {
385  //add the limbs of the shooter to the list of bodies to be ignored
386  //so that the player can't shoot himself
387  IgnoredBodies = ignoredBodies;
388  Vector2 projectilePos = weaponPos;
389  //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel
390  if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking,
391  customPredicate: (Fixture f) => { return IgnoredBodies == null || !IgnoredBodies.Contains(f.Body); }) == null)
392  {
393  //no obstacles -> we can spawn the projectile at the barrel
394  projectilePos = spawnPos;
395  }
396  else if ((weaponPos - spawnPos).LengthSquared() > 0.0001f)
397  {
398  //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle
399  Vector2 newPos = weaponPos - Vector2.Normalize(spawnPos - projectilePos) * Math.Max(Item.body.GetMaxExtent(), 0.1f);
400  if (MathUtils.IsValid(newPos))
401  {
402  projectilePos = newPos;
403  }
404  }
405  Launch(user, projectilePos, rotation, damageMultiplier, launchImpulseModifier);
406  if (createNetworkEvent && !Item.Removed && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
407  {
408 #if SERVER
409  launchRot = rotation;
410  Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
411 #endif
412  }
413  }
414 
415  public bool Use(Character character = null, float launchImpulseModifier = 0f)
416  {
417  if (character != null && !characterUsable) { return false; }
418  if (item.body == null) { return false; }
419  //can't launch if already launched
420  if (StickTarget != null || IsActive) { return false; }
421 
422  float initialRotation = item.body.Rotation;
423  //if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly
424  //but if the item is e.g. being thrown by a character, we need to take the direction into account
425  if (item.body.Dir < 0 && item.ParentInventory is not ItemInventory)
426  {
427  initialRotation -= MathHelper.Pi;
428  }
429  for (int i = 0; i < HitScanCount; i++)
430  {
431  float launchAngle;
432  if (StaticSpread)
433  {
434  launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
435  }
436  else
437  {
438  launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
439  }
440  spreadIndex++;
441 
442  Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
443  if (Hitscan)
444  {
445  Vector2 prevSimpos = item.SimPosition;
447  DoHitscan(launchDir);
448  if (i < HitScanCount - 1)
449  {
450  item.SetTransform(prevSimpos, item.body.Rotation);
451  }
452  }
453  else
454  {
455  item.body.SetTransform(item.body.SimPosition, launchAngle);
456  float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
457  DoLaunch(launchDir * modifiedLaunchImpulse);
458  }
459  }
460  User = character;
461  ApplyStatusEffects(ActionType.OnUse, 1.0f, User, user: User);
462  return true;
463  }
464 
465  public override bool Use(float deltaTime, Character character = null) => Use(character);
466 
467  private void DoLaunch(Vector2 impulse)
468  {
469  hits.Clear();
470 
471  Vector2 prevVelocity = item.body.LinearVelocity;
472 
473  if (item.AiTarget != null)
474  {
475  item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
477  }
478 
479  //do not create a network event about dropping the projectile at this point,
480  //otherwise clients would fail to launch the correct projectile when firing the weapon
481  var prevInventory = item.ParentInventory;
482  if (prevInventory != null && GameMain.NetworkMember is { IsServer: true })
483  {
484  //update the state of the inventory after a delay,
485  //in case a client failed to launch the projectile for some reason
486  CoroutineManager.Invoke(() =>
487  {
488  if (item.Removed) { return; }
489  prevInventory.CreateNetworkEvent();
490  }, delay: CorrectionDelay / 2);
491  }
492  item.Drop(null, createNetworkEvent: false);
493 
495 
496  launchPos = item.SimPosition;
498 
499  item.body.Enabled = true;
500  if (item.body.BodyType == BodyType.Kinematic)
501  {
502  item.body.LinearVelocity = impulse;
503  }
504  else if (impulse.LengthSquared() > 0.001f)
505  {
506  impulse *= item.body.Mass;
507  item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f);
508  }
509  else
510  {
511  //if no impulse is defined, maintain the projectile's original velocity (if any)
512  //can be used to make throwable items behave as projectiles
513  item.body.LinearVelocity = prevVelocity;
514  }
515 
516  item.body.FarseerBody.OnCollision += OnProjectileCollision;
517  item.body.FarseerBody.IsBullet = true;
518 
519  EnableProjectileCollisions();
520 
521  IsActive = true;
522 
523  if (stickJoint == null) { return; }
524 
525  StickTarget = null;
526  GameMain.World.Remove(stickJoint);
527  stickJoint = null;
528  }
529 
530  private void DoHitscan(Vector2 dir)
531  {
532  float rotation = item.body.Rotation;
533  Vector2 simPositon = item.SimPosition;
534  Vector2 rayStartWorld = item.WorldPosition;
535  item.Drop(null, createNetworkEvent: false);
537 
538  item.body.Enabled = true;
539  //set the velocity of the body because the OnProjectileCollision method
540  //uses it to determine the direction from which the projectile hit
541  item.body.LinearVelocity = dir;
542  IsActive = true;
543 
544  Vector2 rayStart = simPositon;
545  Vector2 rayEnd = rayStart + dir * 500.0f;
546 
547  float worldDist = 1000.0f;
548 #if CLIENT
549  worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
550 #endif
551  Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
552 
553  List<HitscanResult> hits = new List<HitscanResult>();
554  hits.AddRange(DoRayCast(rayStart, rayEnd, submarine: item.Submarine));
555 
556  if (item.Submarine != null)
557  {
558  //shooting indoors, do a hitscan outside as well
559  hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition, submarine: null));
560  //do a hitscan in other subs' coordinate spaces
561  RayCastInOtherSubs(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition);
562  }
563  else
564  {
565  RayCastInOtherSubs(rayStart, rayEnd);
566  }
567 
568  void RayCastInOtherSubs(Vector2 rayStart, Vector2 rayEnd)
569  {
570  //shooting outdoors, see if we can hit anything inside a sub
571  foreach (Submarine submarine in Submarine.Loaded)
572  {
573  if (submarine == item.Submarine) { continue; }
574  var inSubHits = DoRayCast(rayStart - submarine.SimPosition, rayEnd - submarine.SimPosition, submarine);
575  //transform back to world coordinates
576  for (int i = 0; i < inSubHits.Count; i++)
577  {
578  inSubHits[i] = new HitscanResult(
579  inSubHits[i].Fixture,
580  inSubHits[i].Point + submarine.SimPosition,
581  inSubHits[i].Normal,
582  inSubHits[i].Fraction);
583  }
584  hits.AddRange(inSubHits);
585  }
586  }
587 
588  int hitCount = 0;
589  Vector2 lastHitPos = item.WorldPosition;
590  hits = hits.OrderBy(h => h.Fraction).ToList();
591  for (int i = 0; i < hits.Count; i++)
592  {
593  var h = hits[i];
594  item.SetTransform(h.Point, rotation);
596  if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
597  {
598  hitCount++;
599  if (hitCount >= MaxTargetsToHit || i == hits.Count - 1)
600  {
601  LaunchProjSpecific(rayStartWorld, item.WorldPosition);
602  break;
603  }
604  }
605  }
606  //the raycast didn't hit anything (or didn't hit enough targets to stop the projectile) -> the projectile flew somewhere outside the level and is permanently lost
607  if (hitCount < MaxTargetsToHit)
608  {
610  LaunchProjSpecific(rayStartWorld, rayEndWorld);
611  if (Entity.Spawner == null)
612  {
613  item.Remove();
614  }
615  else
616  {
617  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
618  {
619  //clients aren't allowed to remove items by themselves, so lets hide the projectile until the server tells us to remove it
621  }
622  else
623  {
624  Entity.Spawner.AddItemToRemoveQueue(item);
625  }
626  }
627  }
628  }
629 
630  private List<HitscanResult> DoRayCast(Vector2 rayStart, Vector2 rayEnd, Submarine submarine)
631  {
632  List<HitscanResult> hits = new List<HitscanResult>();
633 
634  Vector2 dir = rayEnd - rayStart;
635  dir = dir.LengthSquared() < 0.00001f ? Vector2.UnitY : Vector2.Normalize(dir);
636 
637  //do an AABB query first to see if the start of the ray is inside a fixture
638  var aabb = new FarseerPhysics.Collision.AABB(rayStart - Vector2.One * 0.001f, rayStart + Vector2.One * 0.001f);
639  GameMain.World.QueryAABB((fixture) =>
640  {
641  if (fixture?.Body.UserData is LevelObject levelObj)
642  {
643  if (!levelObj.Prefab.TakeLevelWallDamage) { return true; }
644  }
645  else if (fixture?.Body == null || fixture.IsSensor)
646  {
647  //ignore sensors
648  return true;
649  }
650  if (fixture.Body.UserData is VineTile) { return true; }
651  if (fixture.CollidesWith == Category.None) { return true; }
652  //only collides with characters = probably an "outsideCollisionBlocker" created by a gap
653  if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; }
654 
655  if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
656 
657  //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
658  if (submarine != null)
659  {
660  if (fixture.Body.UserData is VoronoiCell) { return true; }
661  if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return true; }
662  }
663 
664  if (fixture.Body.UserData is VoronoiCell && (this.item.Submarine != null || submarine != null)) { return true; }
665 
666  if (fixture.Body.UserData is Item item)
667  {
668  if (item == Item) { return true; }
669  if (item.Condition <= 0) { return true; }
670  if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
671  }
672  else if (fixture.Body.UserData is Holdable { CanPush: false })
673  {
674  // Ignore holdables that can't push -> shouldn't block
675  return true;
676  }
677  else
678  {
679  // TODO: This might make us ignore something we don't want to ignore?
680  // Not item -> ignore everything else than characters, sub walls and level walls
681  if (!fixture.CollisionCategories.HasFlag(Physics.CollisionCharacter) &&
682  !fixture.CollisionCategories.HasFlag(Physics.CollisionWall) &&
683  !fixture.CollisionCategories.HasFlag(Physics.CollisionLevel)) { return true; }
684  }
685 
686  fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
687  if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
688 
689  hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f));
690  return true;
691  }, ref aabb);
692 
693  GameMain.World.RayCast((fixture, point, normal, fraction) =>
694  {
695  //ignore sensors and items
696  if (fixture?.Body.UserData is LevelObject levelObj)
697  {
698  if (!levelObj.Prefab.TakeLevelWallDamage) { return -1; }
699  }
700  else if (fixture?.Body == null || fixture.IsSensor)
701  {
702  //ignore sensors
703  return -1;
704  }
705  if (fixture.Body.UserData is VineTile) { return -1; }
706  if (fixture.CollidesWith == Category.None) { return -1; }
707  //only collides with characters = probably an "outsideCollisionBlocker" created by a gap
708  if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; }
709  if (fixture.Body.UserData is Item item)
710  {
711  if (item.Condition <= 0) { return -1; }
712  if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
713  }
714  if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
715 
716  //if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
717  if (submarine != null)
718  {
719  if (fixture.Body.UserData is VoronoiCell) { return -1; }
720  if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
721  if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; }
722  if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; }
723  }
724 
725  // Ignore holdables that can't push -> shouldn't block
726  if (fixture.Body.UserData is Holdable { CanPush: false })
727  {
728  return -1;
729  }
730 
731  //ignore level cells if the item and the point of impact are inside a sub
732  if (fixture.Body.UserData is VoronoiCell)
733  {
734  if (Hull.FindHull(ConvertUnits.ToDisplayUnits(point), this.item.CurrentHull) != null && this.item.Submarine != null)
735  {
736  return -1;
737  }
738  }
739 
740  if (hits.Count > 50)
741  {
742  float furthestHit = 0.0f;
743  int furthestHitIndex = -1;
744  for (int i = 0; i < hits.Count; i++)
745  {
746  if (hits[i].Fraction > furthestHit)
747  {
748  furthestHitIndex = i;
749  furthestHit = hits[i].Fraction;
750  }
751  }
752  if (furthestHitIndex > -1)
753  {
754  hits.RemoveAt(furthestHitIndex);
755  }
756  }
757 
758  hits.Add(new HitscanResult(fixture, point, normal, fraction));
759 
760  return 1;
761  }, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
762 
763  return hits;
764  }
765 
766  public override void Drop(Character dropper, bool setTransform = true)
767  {
769  if (dropper != null)
770  {
771  DisableProjectileCollisions();
772  Unstick();
773  }
774  base.Drop(dropper, setTransform);
775  }
776 
777  public override void Update(float deltaTime, Camera cam)
778  {
779  if (DeactivationTime > 0)
780  {
781  deactivationTimer -= deltaTime;
782  if (deactivationTimer < 0)
783  {
784  DisableProjectileCollisions();
785  }
786  }
787  while (impactQueue.Count > 0)
788  {
789  var impact = impactQueue.Dequeue();
790  HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity);
791  }
792 
793  if (!removePending)
794  {
795  Entity useTarget = lastTarget?.Body.UserData is Limb limb ? limb.character : lastTarget?.Body.UserData as Entity;
796  ApplyStatusEffects(ActionType.OnActive, deltaTime, useTarget: useTarget, user: _user);
797  }
798 
799  if (item.body != null && item.body.FarseerBody.IsBullet)
800  {
801  if (item.body.LinearVelocity.LengthSquared() < ContinuousCollisionThreshold * ContinuousCollisionThreshold)
802  {
803  item.body.FarseerBody.IsBullet = false;
804  }
805  }
806  //projectiles with a stickjoint don't become inactive until the stickjoint is detached
807  if (stickJoint == null && !item.body.FarseerBody.IsBullet)
808  {
809  IsActive = false;
810  if (DeactivationTime > 0 && deactivationTimer > 0)
811  {
812  DisableProjectileCollisions();
813  }
814  }
815 
816  if (stickJoint == null) { return; }
817 
818  if (StickDuration > 0 && stickTimer > 0)
819  {
820  stickTimer -= deltaTime;
821  return;
822  }
823 
824  float absoluteMaxTranslation = 100;
825  // Update the item's transform to make sure it's inside the same sub as the target (or outside)
826  if (StickTarget?.UserData is Limb target && target.Submarine != item.Submarine || stickJoint is PrismaticJoint prismaticJoint && Math.Abs(prismaticJoint.JointTranslation) > absoluteMaxTranslation)
827  {
828  item.UpdateTransform();
829  }
830 
831  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
832  {
833  if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits)
834  {
835  Unstick();
836 #if SERVER
837  item.CreateServerEvent(this, new EventData(launch: false));
838 #endif
839  }
840  }
841  }
842 
843  private bool StickTargetRemoved()
844  {
845  if (StickTarget == null) { return true; }
846  if (StickTarget.UserData is Limb limb) { return limb.character.Removed; }
847  if (StickTarget.UserData is Entity entity) { return entity.Removed; }
848  return false;
849  }
850 
851  private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact)
852  {
853  if (User != null && User.Removed) { User = null; return false; }
854  if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
855  if (originalCollisionCategories == Category.None && originalCollisionTargets == Category.None) { return false; }
856  //ignore character colliders (the projectile only hits limbs)
857  if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
858  {
859  return false;
860  }
861  if (target.IsSensor) { return false; }
862  if (hits.Contains(target.Body)) { return false; }
863  if (target.Body.UserData is Submarine)
864  {
865  if (ShouldIgnoreSubmarineCollision(ref target, contact)) { return false; }
866  }
867  else if (target.Body.UserData is Limb limb)
868  {
869  if (limb.IsSevered)
870  {
871  //push the severed limb around a bit, but let the projectile pass through it
872  limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
873  return false;
874  }
875  if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
876  {
877  return false;
878  }
879  }
880  else if (target.Body.UserData is Item item)
881  {
882  if (item.Condition <= 0.0f) { return false; }
883  if (!item.Prefab.DamagedByProjectiles)
884  {
885  if (item.GetComponent<Door>() == null)
886  {
887  return false;
888  }
889  }
890  }
891  else if (target.Body.UserData is Holdable { CanPush: false })
892  {
893  // Ignore holdables that can't push -> shouldn't block
894  return false;
895  }
896 
897  //ignore character colliders (the projectile only hits limbs)
898  if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
899  {
900  return false;
901  }
902 
903  hits.Add(target.Body);
904  impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity));
905  IsActive = true;
906  if (RemoveOnHit)
907  {
908  item.body.FarseerBody.ResetDynamics();
909  }
910  if (hits.Count >= MaxTargetsToHit || target.Body.UserData is VoronoiCell)
911  {
912  DisableProjectileCollisions();
913  return true;
914  }
915  else
916  {
917  return false;
918  }
919  }
920 
927  public bool ShouldIgnoreSubmarineCollision(Fixture target, Contact contact)
928  {
929  return ShouldIgnoreSubmarineCollision(ref target, contact);
930  }
931 
932  private bool ShouldIgnoreSubmarineCollision(ref Fixture target, Contact contact)
933  {
934  //not in the projectile category: the projectile has not been launched (e.g. just dropped from an inventory)
935  if (item.body.CollisionCategories != Physics.CollisionProjectile)
936  {
937  return false;
938  }
939  if (target.Body.UserData is Submarine sub)
940  {
941  //hit an item in a different sub -> no need to ignore, we can process the impact with this info
942  //(if it wasn't, we'll move the projectile to that sub's coordinate space and let it hit what it hits there)
943  if (Launcher?.Submarine != sub && target.UserData is Item)
944  {
945  return false;
946  }
947 
948  Vector2 normalizedVel;
949  Vector2 dir;
950  if (item.body.LinearVelocity.LengthSquared() < 0.001f)
951  {
952  normalizedVel = Vector2.Zero;
953  dir = contact.Manifold.LocalNormal;
954  }
955  else
956  {
957  normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
958  }
959 
960  //do a raycast in the sub's coordinate space to see if it hit a structure
961  var wallBody = Submarine.PickBody(
962  item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
963  item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
964  collisionCategory: Physics.CollisionWall);
965 
966  Vector2 launchPosInCurrentCoordinateSpace = launchPos;
967  if (item.body.Submarine == null && LaunchSub != null)
968  {
969  launchPosInCurrentCoordinateSpace += ConvertUnits.ToSimUnits(LaunchSub.Position);
970  }
971  if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
972  //ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
973  Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPosInCurrentCoordinateSpace, dir) > 0)
974  {
975  target = wallBody.FixtureList.First();
976  if (hits.Contains(target.Body))
977  {
978  return true;
979  }
980  }
981  else
982  {
983  return true;
984  }
985  }
986  return false;
987  }
988 
989  private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
990  private Fixture lastTarget;
991 
992  private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
993  {
994  if (User != null && User.Removed) { User = null; }
995  if (IgnoredBodies != null && IgnoredBodies.Contains(target.Body)) { return false; }
996  //ignore character colliders (the projectile only hits limbs)
997  if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character)
998  {
999  return false;
1000  }
1001  lastTarget = target;
1002 
1003  int remainingHits = Math.Max(MaxTargetsToHit - hits.Count, 0);
1004  float speedMultiplier = Math.Min(0.4f + remainingHits * 0.1f, 1.0f);
1005  float deflectedSpeedMultiplier = 0.1f;
1006 
1007  AttackResult attackResult = new AttackResult();
1008  Character character = null;
1009  if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item)
1010  {
1011  item.Move(-submarine.Position, ignoreContacts: false);
1012  item.Submarine = submarine;
1013  item.body.Submarine = submarine;
1014  return !Hitscan;
1015  }
1016  else if (target.Body.UserData is Limb limb)
1017  {
1018  if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
1019  {
1020  return false;
1021  }
1022  // when hitting limbs with piercing ammo, don't lose as much speed
1023  if (MaxTargetsToHit > 1)
1024  {
1025  speedMultiplier = 1f;
1026  deflectedSpeedMultiplier = 0.8f;
1027  }
1028  if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; }
1029 
1030  limb.character.LastDamageSource = item;
1031  if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); }
1032  if (limb.character != null) { character = limb.character; }
1033  }
1034  else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem)
1035  {
1036  if (targetItem.Removed) { return false; }
1037  //hit the external collider of an item (turret?) of the same sub -> ignore
1038  if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; }
1039  if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent<Door>() != null) && targetItem.Condition > 0)
1040  {
1041  attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
1042 #if CLIENT
1043  if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
1044  (User == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
1045  {
1046  Character.Controlled.UpdateHUDProgressBar(targetItem,
1047  targetItem.WorldPosition,
1048  targetItem.Condition / targetItem.MaxCondition,
1049  emptyColor: GUIStyle.HealthBarColorLow,
1050  fullColor: GUIStyle.HealthBarColorHigh,
1051  textTag: targetItem.Prefab.ShowNameInHealthBar ? targetItem.Name : string.Empty);
1052  }
1053 #endif
1054  }
1055  }
1056  else if (target.Body.UserData is IDamageable damageable)
1057  {
1058  if (Attack != null)
1059  {
1060  Vector2 pos = item.WorldPosition;
1061  if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
1062  {
1063  item.Submarine = structure.Submarine;
1064  }
1065  attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
1066  }
1067  }
1068  else if (target.Body.UserData is VoronoiCell voronoiCell && voronoiCell.IsDestructible && Attack != null && Math.Abs(Attack.LevelWallDamage) > 0.0f)
1069  {
1070  if (Level.Loaded?.ExtraWalls.Find(w => w.Body == target.Body) is DestructibleLevelWall destructibleWall)
1071  {
1072  attackResult = Attack.DoDamage(User ?? Attacker, destructibleWall, item.WorldPosition, 1.0f);
1073  }
1074  }
1075 
1076  if (character != null) { character.LastDamageSource = item; }
1077 
1078  ActionType conditionalActionType = ActionType.OnSuccess;
1079  if (User != null && Rand.Range(0.0f, 0.5f) > DegreeOfSuccess(User))
1080  {
1081  conditionalActionType = ActionType.OnFailure;
1082  }
1083 #if CLIENT
1084  PlaySound(conditionalActionType, user: User);
1085  PlaySound(ActionType.OnImpact, user: User);
1086 #endif
1087 
1088  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
1089  {
1090  if (target.Body.UserData is Limb targetLimb)
1091  {
1092  ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User);
1093  ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User);
1094  var attack = targetLimb.attack;
1095  if (attack != null)
1096  {
1097  // Apply the status effects defined in the limb's attack that was hit
1098  foreach (var effect in attack.StatusEffects)
1099  {
1100  if (effect.type == ActionType.OnImpact)
1101  {
1102  if (effect.HasTargetType(StatusEffect.TargetType.This))
1103  {
1104  effect.Apply(effect.type, 1.0f, User, User);
1105  }
1106  if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget))
1107  {
1108  effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character);
1109  }
1110  if (effect.HasTargetType(StatusEffect.TargetType.Limb))
1111  {
1112  effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb);
1113  }
1114  if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
1115  effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
1116  {
1117  targets.Clear();
1118  effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
1119  effect.Apply(effect.type, 1.0f, targetLimb.character, targets);
1120  }
1121  }
1122  }
1123  }
1124  if (GameMain.NetworkMember is { IsServer: true } server)
1125  {
1126  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
1127  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, targetLimb.character, targetLimb, useTarget: targetLimb.character, item.WorldPosition));
1128  }
1129  }
1130  else
1131  {
1132  ApplyStatusEffects(conditionalActionType, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
1133  ApplyStatusEffects(ActionType.OnImpact, 1.0f, useTarget: target.Body.UserData as Entity, user: User);
1134  if (GameMain.NetworkMember is { IsServer: true } server)
1135  {
1136  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
1137  server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnImpact, this, useTarget: target.Body.UserData as Entity, worldPosition: item.WorldPosition));
1138  }
1139  }
1140  }
1141 
1142  target.Body.ApplyLinearImpulse(velocity * item.body.Mass);
1143  target.Body.LinearVelocity = target.Body.LinearVelocity.ClampLength(NetConfig.MaxPhysicsBodyVelocity * 0.5f);
1144 
1145  if (hits.Count >= MaxTargetsToHit || hits.LastOrDefault()?.UserData is VoronoiCell)
1146  {
1147  DisableProjectileCollisions();
1148  }
1149 
1150  if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles) && !StickToDeflective)
1151  {
1152  item.body.LinearVelocity *= deflectedSpeedMultiplier;
1153  }
1154  else if ( remainingHits <= 0 &&
1155  stickJoint == null && StickTarget == null &&
1156  StickToStructures && target.Body.UserData is Structure ||
1157  ((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
1158  (DoesStick ||
1159  (StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
1160  (target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
1161  {
1162  Vector2 dir = new Vector2(
1163  (float)Math.Cos(item.body.Rotation),
1164  (float)Math.Sin(item.body.Rotation));
1165 
1166  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
1167  {
1168  if (target.Body.UserData is Structure structure && structure.Submarine != item.Submarine && structure.Submarine != null)
1169  {
1170  StickToTarget(structure.Submarine.PhysicsBody.FarseerBody, dir);
1171  }
1172  else
1173  {
1174  StickToTarget(target.Body, dir);
1175  }
1176  }
1177 #if SERVER
1178  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
1179  {
1180  item.CreateServerEvent(this, new EventData(launch: false));
1181  }
1182 #endif
1183  item.body.LinearVelocity *= speedMultiplier;
1184 
1185  return Hitscan;
1186  }
1187  else
1188  {
1189  item.body.LinearVelocity *= speedMultiplier;
1190  }
1191 
1192  var containedItems = item.OwnInventory?.AllItems;
1193  if (containedItems != null)
1194  {
1195  foreach (Item contained in containedItems)
1196  {
1197  if (contained.body != null)
1198  {
1199  contained.SetTransform(item.SimPosition, contained.body.Rotation);
1200  }
1201  }
1202  }
1203 
1204  if (RemoveOnHit)
1205  {
1206  removePending = true;
1207  item.HiddenInGame = true;
1208  item.body.FarseerBody.Enabled = false;
1209  //delete with a brief delay so we don't delete the item
1210  //before a client has managed to launch it (can happen with hitscan projectile)
1211  CoroutineManager.Invoke(() =>
1212  {
1213  if (item.Removed) { return; }
1214  Entity.Spawner?.AddItemToRemoveQueue(item);
1215  }, delay: 0.5f);
1216  }
1217 
1218  return true;
1219  }
1220 
1221  private void EnableProjectileCollisions()
1222  {
1223  if (item.body.CollisionCategories != Category.None)
1224  {
1225  item.body.CollisionCategories = Physics.CollisionProjectile;
1226  item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking;
1227  }
1228  if (item.Prefab.DamagedByProjectiles && !IgnoreProjectilesWhileActive)
1229  {
1230  if (item.body.CollisionCategories == Category.None) { item.body.CollisionCategories = Physics.CollisionCharacter; }
1231  item.body.CollidesWith |= Physics.CollisionProjectile;
1232  }
1233  }
1234 
1235  private void DisableProjectileCollisions()
1236  {
1237  if (item?.body?.FarseerBody == null) { return; }
1238  item.body.FarseerBody.OnCollision -= OnProjectileCollision;
1239  if (originalCollisionCategories != Category.None && originalCollisionTargets != Category.None)
1240  {
1241  item.body.CollisionCategories = originalCollisionCategories;
1242  item.body.CollidesWith = originalCollisionTargets;
1243  }
1244  else
1245  {
1246  if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0)
1247  {
1248  item.body.CollisionCategories = Physics.CollisionCharacter;
1249  item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile;
1250  }
1251  else
1252  {
1253  item.body.CollisionCategories = Physics.CollisionItem;
1254  item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
1255  }
1256  }
1257  IgnoredBodies?.Clear();
1258  }
1259 
1260  public bool IsAttachedTo(PhysicsBody body)
1261  {
1262  return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody);
1263  }
1264 
1265  private void StickToTarget(Body targetBody, Vector2 axis)
1266  {
1267  if (stickJoint != null) { return; }
1268  jointAxis = axis;
1269  item.body.ResetDynamics();
1270  if (Prismatic)
1271  {
1272  stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, useWorldCoordinates: true)
1273  {
1274  MotorEnabled = true,
1275  MaxMotorForce = 30.0f,
1276  LimitEnabled = true,
1277  Breakpoint = 1000.0f
1278  };
1279 
1280  if (maxJointTranslationInSimUnits == -1)
1281  {
1282  if (item.Sprite != null && MaxJointTranslation < 0)
1283  {
1284  MaxJointTranslation = item.Sprite.size.X / 2 * item.Scale;
1285  }
1286  MaxJointTranslation = Math.Min(MaxJointTranslation, 1000);
1287  maxJointTranslationInSimUnits = ConvertUnits.ToSimUnits(MaxJointTranslation);
1288  }
1289  }
1290  else
1291  {
1292  stickJoint = new WeldJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, item.body.SimPosition, useWorldCoordinates: true)
1293  {
1294  FrequencyHz = 10.0f,
1295  DampingRatio = 0.5f
1296  };
1297  }
1298  stickTimer = StickDuration;
1299  StickTarget = targetBody;
1300  GameMain.World.Add(stickJoint);
1301  IsActive = true;
1302  if (targetBody.UserData is Limb limb)
1303  {
1304  stickTargetCharacter = limb.character;
1305  stickTargetCharacter.AttachedProjectiles.Add(this);
1306  }
1307  }
1308 
1309  public void Unstick()
1310  {
1311  StickTarget = null;
1312  if (stickJoint != null)
1313  {
1314  if (GameMain.World.JointList.Contains(stickJoint))
1315  {
1316  GameMain.World.Remove(stickJoint);
1317  }
1318  stickJoint = null;
1319  }
1320  if (!item.body.FarseerBody.IsBullet)
1321  {
1322  IsActive = false;
1323  if (DeactivationTime > 0 && deactivationTimer > 0)
1324  {
1325  DisableProjectileCollisions();
1326  }
1327  }
1328  item.GetComponent<Rope>()?.Snap();
1329  if (stickTargetCharacter != null)
1330  {
1331  stickTargetCharacter.AttachedProjectiles.Remove(this);
1332  stickTargetCharacter = null;
1333  }
1334  }
1335 
1336  protected override void RemoveComponentSpecific()
1337  {
1338  base.RemoveComponentSpecific();
1339  if (IsStuckToTarget || stickJoint != null || stickTargetCharacter != null)
1340  {
1341  Unstick();
1342  }
1343  }
1344  partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation);
1345  }
1346 }
Attacks are used to deal damage to characters, structures and items. They can be defined in the weapo...
float DamageMultiplier
Used for multiplying all the damage.
AITarget AiTarget
Definition: Entity.cs:55
Submarine Submarine
Definition: Entity.cs:53
static World World
Definition: GameMain.cs:105
static NetworkMember NetworkMember
Definition: GameMain.cs:190
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
void ResetWaterDragCoefficient()
Removes the override value -> falls back to using the original value defined in the xml.
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
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)
bool Use(Character character=null, float launchImpulseModifier=0f)
bool ShouldIgnoreSubmarineCollision(Fixture target, Contact contact)
Should the collision with the target submarine be ignored (e.g. did the projectile collide with the w...
void Shoot(Character user, Vector2 weaponPos, Vector2 spawnPos, float rotation, List< Body > ignoredBodies, bool createNetworkEvent, float damageMultiplier=1f, float launchImpulseModifier=0f)
override bool Use(float deltaTime, Character character=null)
override void Drop(Character dropper, bool setTransform=true)
a Character has dropped the item
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
Mersenne Twister based random
Definition: MTRandom.cs:9
override double NextDouble()
Returns random value larger or equal to 0.0 and less than 1.0
Definition: MTRandom.cs:122
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
bool SetTransformIgnoreContacts(Vector2 simPosition, float rotation, bool setPrevTransform=true)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19