Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Rope.cs
3 using FarseerPhysics;
4 using FarseerPhysics.Dynamics;
5 using Microsoft.Xna.Framework;
6 using System;
7 
9 {
11  {
12  private ISpatialEntity source;
13  private Item target;
14  private Vector2? launchDir;
15  private float currentRopeLength;
16 
17  private void SetSource(ISpatialEntity source)
18  {
19  this.source = source;
20  if (source is Limb sourceLimb)
21  {
22  sourceLimb.AttachedRope = this;
23  float offset = sourceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
24  launchDir = VectorExtensions.Forward(sourceLimb.body.TransformedRotation - offset * sourceLimb.character.AnimController.Dir);
25  }
26  }
27 
28  private void ResetSource()
29  {
30  if (source is Limb sourceLimb && sourceLimb.AttachedRope == this)
31  {
32  sourceLimb.AttachedRope = null;
33  }
34  source = null;
35  }
36 
37  private float snapTimer;
38 
39  [Serialize(1.0f, IsPropertySaveable.No, description: "")]
40  public float SnapAnimDuration
41  {
42  get;
43  set;
44  }
45 
46  private float raycastTimer;
47  private const float RayCastInterval = 0.2f;
48 
49  [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the projectile the rope is attached to.")]
50  public float ProjectilePullForce
51  {
52  get;
53  set;
54  }
55 
56  [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the target the rope is attached to.")]
57  public float TargetPullForce
58  {
59  get;
60  set;
61  }
62 
63  [Serialize(0.0f, IsPropertySaveable.No, description: "How much force is applied to pull the source the rope is attached to.")]
64  public float SourcePullForce
65  {
66  get;
67  set;
68  }
69 
70  [Serialize(1000.0f, IsPropertySaveable.No, description: "How far the source item can be from the projectile until the rope breaks.")]
71  public float MaxLength
72  {
73  get;
74  set;
75  }
76 
77  [Serialize(200.0f, IsPropertySaveable.No, description: "At which distance the user stops pulling the target?")]
78  public float MinPullDistance
79  {
80  get;
81  set;
82  }
83 
84  [Serialize(360.0f, IsPropertySaveable.No, description: "The maximum angle from the source to the target until the rope breaks.")]
85  public float MaxAngle
86  {
87  get;
88  set;
89  }
90 
91  [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")]
92  public bool SnapOnCollision
93  {
94  get;
95  set;
96  }
97 
98  [Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the character drops the aim?")]
99  public bool SnapWhenNotAimed
100  {
101  get;
102  set;
103  }
104 
105  [Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
106  public float TargetMinMass
107  {
108  get;
109  set;
110  }
111 
112  [Serialize(false, IsPropertySaveable.No)]
113  public bool LerpForces
114  {
115  get;
116  set;
117  }
118 
119  private bool isReelingIn;
120  private bool snapped;
121  public bool Snapped
122  {
123  get { return snapped; }
124  set
125  {
126  if (snapped == value) { return; }
127  if (GameMain.NetworkMember != null)
128  {
129  if (GameMain.NetworkMember.IsClient)
130  {
131  return;
132  }
133  else
134  {
135 #if SERVER
136  item.CreateServerEvent(this);
137 #endif
138  }
139  }
140  snapped = value;
141  if (!snapped)
142  {
143  snapTimer = 0;
144  }
145  else if (target != null && source != null && target != source)
146  {
147 #if CLIENT
148  // Play a sound at both ends. Initially tested playing the sound in the middle when the rope snaps in the middle,
149  // but I think it's more important to ensure that the players hear the sound.
150  PlaySound(snapSound, source.WorldPosition);
151  PlaySound(snapSound, target.WorldPosition);
152 #endif
153  }
154  }
155  }
156 
157  public Rope(Item item, ContentXElement element) : base(item, element)
158  {
159  InitProjSpecific(element);
160  }
161 
162  partial void InitProjSpecific(ContentXElement element);
163 
164  public void Snap() => Snapped = true;
165 
166  public void Attach(ISpatialEntity source, Item target)
167  {
168  System.Diagnostics.Debug.Assert(source != null);
169  System.Diagnostics.Debug.Assert(target != null);
170  this.target = target;
171  SetSource(source);
172  Snapped = false;
173  ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition);
174  IsActive = true;
175  }
176 
177  public override void Update(float deltaTime, Camera cam)
178  {
179  UpdateProjSpecific();
180  isReelingIn = false;
181  Character user = item.GetComponent<Projectile>()?.User;
182  if (source == null || target == null || target.Removed ||
183  source is Entity { Removed: true } ||
184  source is Limb { Removed: true } ||
185  user is { Removed: true })
186  {
187  ResetSource();
188  target = null;
189  IsActive = false;
190  return;
191  }
192 
193  if (Snapped)
194  {
195  snapTimer += deltaTime;
196  if (snapTimer >= SnapAnimDuration)
197  {
198  IsActive = false;
199  }
200  return;
201  }
202 
203  Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false);
204  float lengthSqr = diff.LengthSquared();
205  if (lengthSqr > MaxLength * MaxLength)
206  {
207  Snap();
208  return;
209  }
210 
211  if (MaxAngle < 180 && lengthSqr > 2500)
212  {
213  launchDir ??= diff;
214  float angle = MathHelper.ToDegrees(launchDir.Value.Angle(diff));
215  if (angle > MaxAngle)
216  {
217  Snap();
218  return;
219  }
220  }
221 
222 #if CLIENT
224 #endif
225  var projectile = target.GetComponent<Projectile>();
226  if (projectile == null) { return; }
227 
228  if (SnapOnCollision)
229  {
230  raycastTimer += deltaTime;
231  if (raycastTimer > RayCastInterval)
232  {
233  if (Submarine.PickBody(ConvertUnits.ToSimUnits(source.WorldPosition), ConvertUnits.ToSimUnits(target.WorldPosition),
234  collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
235  customPredicate: (Fixture f) =>
236  {
237  foreach (Body body in projectile.Hits)
238  {
239  Submarine alreadyHitSub = null;
240  if (body.UserData is Structure hitStructure)
241  {
242  alreadyHitSub = hitStructure.Submarine;
243  }
244  else if (body.UserData is Submarine hitSub)
245  {
246  alreadyHitSub = hitSub;
247  }
248  if (alreadyHitSub != null)
249  {
250  if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; }
251  if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; }
252  }
253  }
254  Submarine targetSub = projectile.StickTarget?.UserData as Submarine ?? target.Submarine;
255 
256  if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null)
257  {
258  if (mapEntity.Submarine == targetSub || mapEntity.Submarine == source.Submarine)
259  {
260  return false;
261  }
262  }
263  else if (f.Body?.UserData is Submarine sub)
264  {
265  if (sub == targetSub || sub == source.Submarine)
266  {
267  return false;
268  }
269  }
270  return true;
271  }) != null)
272  {
273  Snap();
274  return;
275  }
276  raycastTimer = 0.0f;
277  }
278  }
279 
280  Vector2 forceDir = diff;
281  currentRopeLength = diff.Length();
282  if (currentRopeLength > 0.001f)
283  {
284  forceDir = Vector2.Normalize(forceDir);
285  }
286 
287  if (Math.Abs(ProjectilePullForce) > 0.001f)
288  {
289  projectile.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce);
290  }
291 
292  if (projectile.StickTarget != null)
293  {
294  float targetMass = float.MaxValue;
295  Character targetCharacter = null;
296  switch (projectile.StickTarget.UserData)
297  {
298  case Limb targetLimb:
299  targetCharacter = targetLimb.character;
300  targetMass = targetLimb.ragdoll.Mass;
301  break;
302  case Character character:
303  targetCharacter = character;
304  targetMass = character.Mass;
305  break;
306  case Item _:
307  targetMass = projectile.StickTarget.Mass;
308  break;
309  }
310  if (projectile.StickTarget.BodyType != BodyType.Dynamic)
311  {
312  targetMass = float.MaxValue;
313  }
314  // Currently can only apply pull forces to the source, when it's a character, not e.g. when the item would be auto-operated by an AI. Might have to change this.
315  if (user != null)
316  {
317  if (!snapped)
318  {
319  user.AnimController.HoldToRope();
320  if (targetCharacter != null)
321  {
322  targetCharacter.AnimController.DragWithRope();
323  }
324  if (user.InWater)
325  {
326  user.AnimController.HangWithRope();
327  }
328  }
329  if (Math.Abs(SourcePullForce) > 0.001f && targetMass > TargetMinMass)
330  {
331  // This should be the main collider.
332  var sourceBody = GetBodyToPull(source);
333  if (sourceBody != null)
334  {
335  isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
336  if (isReelingIn)
337  {
338  float pullForce = SourcePullForce;
339  if (!user.InWater)
340  {
341  // Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
342  pullForce *= 0.1f;
343  }
344  float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
345  float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
346  sourceBody.ApplyForce(forceDir * force);
347  // Take the target velocity into account.
348  PhysicsBody targetBody = GetBodyToPull(target);
349  if (targetBody != null)
350  {
351  if (targetCharacter != null)
352  {
353  if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
354  {
355  Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
356  float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
357  if (movementDot < 0)
358  {
359  // Pushing to a different dir -> add some counter force
360  const float multiplier = 5;
361  float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
362  sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
363  }
364  float forceDot = Vector2.Dot(forceDir, targetDir);
365  if (forceDot > 0)
366  {
367  // Pulling to the same dir -> add extra force
368  float targetSpeed = targetBody.LinearVelocity.Length();
369  const float multiplier = 25;
370  sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
371  }
372  float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
373  const float minDist = 1;
374  const float maxDist = 10;
375  if (colliderMainLimbDistance > minDist)
376  {
377  // Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
378  float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
379  Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
380  user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
381  }
382  }
383  }
384  else
385  {
386  sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
387  }
388  }
389  }
390  }
391  }
392  }
393 
394  if (Math.Abs(TargetPullForce) > 0.001f && user is not { IsRagdolled: true})
395  {
396  PhysicsBody targetBody = GetBodyToPull(target);
397  if (targetBody == null) { return; }
398  bool lerpForces = LerpForces;
399  float maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.25f;
400  // The distance where we start pulling with max force.
401  float maxPullDistance = MaxLength / 3;
402  float minPullDistance = MinPullDistance;
403  const float absoluteMinPullDistance = 50;
404  if (targetCharacter != null)
405  {
406  if (targetCharacter.IsRagdolled || targetCharacter.IsUnconscious)
407  {
408  if (!targetCharacter.InWater)
409  {
410  // Limits the velocity of ragdolled characters on ground/air, because otherwise they tend to move with too high forces.
411  maxVelocity = NetConfig.MaxPhysicsBodyVelocity * 0.075f;
412  }
413  }
414  else
415  {
416  // Target alive and kicking -> Use the absolute min pull distance and full forces to pull.
417  // Keep some lerping, because it results into smoothing when the target is close by.
418  minPullDistance = absoluteMinPullDistance;
419  maxPullDistance = 200;
420  }
421  }
422  minPullDistance = MathHelper.Max(minPullDistance, absoluteMinPullDistance);
423  if (currentRopeLength < minPullDistance) { return; }
424  maxPullDistance = MathHelper.Max(minPullDistance * 2, maxPullDistance);
425  float force = lerpForces
426  ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(minPullDistance, maxPullDistance, currentRopeLength))
427  : TargetPullForce;
428  targetBody.ApplyForce(-forceDir * force, maxVelocity);
429  AnimController targetRagdoll = targetCharacter?.AnimController;
430  if (targetRagdoll?.Collider != null)
431  {
432  isReelingIn = true;
433  if (targetRagdoll.InWater || targetRagdoll.OnGround)
434  {
435  float forceMultiplier = 1;
436  if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated)
437  {
438  // Pulling the main collider requires higher forces when the target is trying to move away.
439  Vector2 targetMovement = targetCharacter.AnimController.TargetMovement;
440  float dot = Vector2.Dot(Vector2.Normalize(targetMovement), forceDir);
441  if (dot > 0)
442  {
443  const float constMultiplier = 2.5f;
444  float targetVelocity = targetMovement.Length();
445  float massFactor = Math.Max((float)Math.Log(targetCharacter.Mass / 10), 1);
446  forceMultiplier = Math.Max(targetVelocity * massFactor * constMultiplier * dot, 1);
447  }
448  }
449  targetRagdoll.Collider.ApplyForce(-forceDir * force * forceMultiplier, maxVelocity);
450  }
451  }
452  }
453  }
454  }
455 
456  partial void UpdateProjSpecific();
457 
458  public override void UpdateBroken(float deltaTime, Camera cam)
459  {
460  base.UpdateBroken(deltaTime, cam);
461  if (Snapped)
462  {
463  snapTimer += deltaTime;
464  if (snapTimer >= SnapAnimDuration)
465  {
466  IsActive = false;
467  }
468  }
469  }
470 
475  private Vector2 GetSourcePos(bool useDrawPosition = false)
476  {
477  Vector2 sourcePos = source.WorldPosition;
478  if (source is Item sourceItem)
479  {
480  if (useDrawPosition)
481  {
482  sourcePos = sourceItem.DrawPosition;
483  }
484  if (!sourceItem.Removed)
485  {
486  if (sourceItem.GetComponent<Turret>() is { } turret)
487  {
488  sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
489  }
490  else if (sourceItem.GetComponent<RangedWeapon>() is { } weapon)
491  {
492  sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos);
493  }
494  }
495  }
496  else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null)
497  {
498  sourcePos = sourceLimb.body.DrawPosition;
499  }
500  return sourcePos;
501  }
502 
503 
504  private static PhysicsBody GetBodyToPull(ISpatialEntity target)
505  {
506  if (target is Item targetItem)
507  {
508  if (targetItem.ParentInventory is CharacterInventory { Owner: Character ownerCharacter })
509  {
510  if (ownerCharacter.Removed) { return null; }
511  return ownerCharacter.AnimController.Collider;
512  }
513  var projectile = targetItem.GetComponent<Projectile>();
514  if (projectile is { StickTarget: not null })
515  {
516  return projectile.StickTarget.UserData switch
517  {
518  Structure structure => structure.Submarine?.PhysicsBody,
519  Submarine sub => sub.PhysicsBody,
520  Item item => item.body,
521  Limb limb => limb.body,
522  _ => null
523  };
524  }
525  if (targetItem.body != null) { return targetItem.body; }
526  }
527  else if (target is Limb targetLimb)
528  {
529  return targetLimb.body;
530  }
531 
532  return null;
533  }
534  }
535 }
Submarine Submarine
Definition: Entity.cs:53
static NetworkMember NetworkMember
Definition: GameMain.cs:190
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)
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19