Server LuaCsForBarotrauma
SubmarineBody.cs
3 using FarseerPhysics;
4 using FarseerPhysics.Common;
5 using FarseerPhysics.Dynamics;
6 using FarseerPhysics.Dynamics.Contacts;
7 using FarseerPhysics.Dynamics.Joints;
8 using Microsoft.Xna.Framework;
9 using System;
10 using System.Collections.Generic;
11 using System.Diagnostics;
12 using System.Linq;
13 using Voronoi2;
14 
15 namespace Barotrauma
16 {
17  partial class SubmarineBody
18  {
19  public const float NeutralBallastPercentage = 0.07f;
20 
21  public const Category CollidesWith =
22  Physics.CollisionItem |
23  Physics.CollisionLevel |
24  Physics.CollisionCharacter |
25  Physics.CollisionProjectile |
26  Physics.CollisionWall;
27 
28  const float HorizontalDrag = 0.01f;
29  const float VerticalDrag = 0.05f;
30  const float MaxDrag = 0.1f;
31 
32  private const float ImpactDamageMultiplier = 10.0f;
33 
34  //limbs with a mass smaller than this won't cause an impact when they hit the sub
35  private const float MinImpactLimbMass = 10.0f;
36  //impacts smaller than this are ignored
37  private const float MinCollisionImpact = 3.0f;
38  //impacts are clamped below this value
39  private const float MaxCollisionImpact = 5.0f;
40  private const float Friction = 0.2f, Restitution = 0.0f;
41 
42  private readonly List<Contact> levelContacts = new List<Contact>();
43 
44  public List<Vector2> HullVertices
45  {
46  get;
47  private set;
48  }
49 
50  private float depthDamageTimer = 10.0f;
51  private float damageSoundTimer = 10.0f;
52 
53  private readonly Submarine submarine;
54 
55  public readonly PhysicsBody Body;
56 
57  private readonly List<PosInfo> positionBuffer = new List<PosInfo>();
58 
59  private readonly Queue<Impact> impactQueue = new Queue<Impact>();
60 
61  private float forceUpwardsTimer;
62  private const float ForceUpwardsDelay = 30.0f;
63 
64  struct Impact
65  {
66  public Fixture Target;
67  public Vector2 Velocity;
68  public Vector2 ImpactPos;
69  public Vector2 Normal;
70 
71  public Impact(Fixture f1, Fixture f2, Contact contact)
72  {
73  Target = f2;
74  contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
75  if (contact.FixtureA.Body == f1.Body) { contactNormal = -contactNormal; }
76  ImpactPos = points[0];
77  Normal = contactNormal;
78  Velocity = f1.Body.LinearVelocity - f2.Body.LinearVelocity;
79  }
80  }
81 
85  public Rectangle Borders
86  {
87  get;
88  private set;
89  }
90 
94  public Rectangle VisibleBorders
95  {
96  get;
97  private set;
98  }
99 
100  public Vector2 Velocity
101  {
102  get { return Body.LinearVelocity; }
103  set
104  {
105  if (!MathUtils.IsValid(value)) return;
106  Body.LinearVelocity = value;
107  }
108  }
109 
110  public Vector2 Position
111  {
112  get { return ConvertUnits.ToDisplayUnits(Body.SimPosition); }
113  }
114 
115  public List<PosInfo> PositionBuffer
116  {
117  get { return positionBuffer; }
118  }
119 
121  {
122  get { return submarine; }
123  }
124 
125  public SubmarineBody(Submarine sub, bool showErrorMessages = true)
126  {
127  this.submarine = sub;
128 
129  Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero;
130  Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero;
131  Body farseerBody = null;
132  if (!Hull.HullList.Any(h => h.Submarine == sub))
133  {
134  farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
135  if (showErrorMessages)
136  {
137  DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed.");
138  }
139  }
140  else
141  {
142  List<Vector2> convexHull = GenerateConvexHull();
143  for (int i = 0; i < convexHull.Count; i++)
144  {
145  convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
146  }
147  HullVertices = convexHull;
148 
149 
150  farseerBody = GameMain.World.CreateBody(findNewContacts: false, bodyType: BodyType.Dynamic);
151  var collisionCategory = Physics.CollisionWall;
152  var collidesWith =
153  Physics.CollisionItem |
154  Physics.CollisionLevel |
155  Physics.CollisionCharacter |
156  Physics.CollisionProjectile |
157  Physics.CollisionWall;
158  farseerBody.CollisionCategories = collisionCategory;
159  farseerBody.CollidesWith = collidesWith;
160  farseerBody.Enabled = false;
161  farseerBody.UserData = this;
162  if (sub.Info.IsOutpost)
163  {
164  farseerBody.BodyType = BodyType.Static;
165  }
166  foreach (var mapEntity in MapEntity.MapEntityList)
167  {
168  if (mapEntity.Submarine != submarine || mapEntity is not Structure wall) { continue; }
169 
170  bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None;
171  Rectangle rect = wall.Rect;
172 
173  var transformedQuad = wall.GetTransformedQuad();
174  AddPointToExtents(transformedQuad.A, hasCollider: hasCollider);
175  AddPointToExtents(transformedQuad.B, hasCollider: hasCollider);
176  AddPointToExtents(transformedQuad.C, hasCollider: hasCollider);
177  AddPointToExtents(transformedQuad.D, hasCollider: hasCollider);
178  if (hasCollider)
179  {
180  farseerBody.CreateRectangle(
181  ConvertUnits.ToSimUnits(wall.BodyWidth),
182  ConvertUnits.ToSimUnits(wall.BodyHeight),
183  50.0f,
184  -wall.BodyRotation,
185  ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset),
186  collisionCategory,
187  collidesWith).UserData = wall;
188  }
189  }
190 
191  foreach (Hull hull in Hull.HullList)
192  {
193  if (hull.Submarine != submarine || hull.IdFreed) { continue; }
194 
195  Rectangle rect = hull.Rect;
196  AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true);
197  AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true);
198 
199  farseerBody.CreateRectangle(
200  ConvertUnits.ToSimUnits(rect.Width),
201  ConvertUnits.ToSimUnits(rect.Height),
202  100.0f,
203  ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2)),
204  collisionCategory,
205  collidesWith).UserData = hull;
206  }
207 
208  foreach (Item item in Item.ItemList)
209  {
210  if (item.Submarine != submarine) { continue; }
211 
212  Vector2 simPos = ConvertUnits.ToSimUnits(item.Position);
213  if (sub.FlippedX) { simPos.X = -simPos.X; }
214  if (item.GetComponent<Door>() is Door door)
215  {
216  door.OutsideSubmarineFixture = farseerBody.CreateRectangle(door.Body.Width, door.Body.Height, 5.0f, simPos, collisionCategory, collidesWith);
217  door.OutsideSubmarineFixture.UserData = item;
218  }
219 
220  if (item.StaticBodyConfig == null) { continue; }
221 
222  float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale;
223  float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale;
224  float height = item.StaticBodyConfig.GetAttributeFloat("height", 0.0f) * item.Scale;
225 
226  float simRadius = ConvertUnits.ToSimUnits(radius);
227  float simWidth = ConvertUnits.ToSimUnits(width);
228  float simHeight = ConvertUnits.ToSimUnits(height);
229 
230  if (radius > 0f || (width > 0f && height > 0f))
231  {
232  var transformedQuad = item.GetTransformedQuad();
233  AddPointToExtents(transformedQuad.A, hasCollider: true);
234  AddPointToExtents(transformedQuad.B, hasCollider: true);
235  AddPointToExtents(transformedQuad.C, hasCollider: true);
236  AddPointToExtents(transformedQuad.D, hasCollider: true);
237  }
238 
239  if (width > 0.0f && height > 0.0f)
240  {
241  item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith));
242  AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true);
243  AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true);
244  }
245  else if (radius > 0.0f && width > 0.0f)
246  {
247  item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith));
248  item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
249  item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
250  AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true);
251  AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
252  }
253  else if (radius > 0.0f && height > 0.0f)
254  {
255  item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith));
256  item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
257  item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
258  AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true);
259  AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
260  }
261  else if (radius > 0.0f)
262  {
263  item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith));
264  AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true);
265  AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true);
266  }
267  item.StaticFixtures.ForEach(f => f.UserData = item);
268  }
269 
270  Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
271  VisibleBorders = new Rectangle((int)visibleMinExtents.X, (int)visibleMaxExtents.Y, (int)(visibleMaxExtents.X - visibleMinExtents.X), (int)(visibleMaxExtents.Y - visibleMinExtents.Y));
272  }
273 
274  farseerBody.Enabled = true;
275  farseerBody.Restitution = Restitution;
276  farseerBody.Friction = Friction;
277  farseerBody.FixedRotation = true;
278  farseerBody.Awake = true;
279  farseerBody.SleepingAllowed = false;
280  farseerBody.IgnoreGravity = true;
281  farseerBody.OnCollision += OnCollision;
282  farseerBody.UserData = submarine;
283 
284  Body = new PhysicsBody(farseerBody);
285 
286  void AddPointToExtents(Vector2 point, bool hasCollider)
287  {
288  visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X);
289  visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y);
290  visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X);
291  visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y);
292  if (hasCollider)
293  {
294  minExtents.X = Math.Min(point.X, minExtents.X);
295  minExtents.Y = Math.Min(point.Y, minExtents.Y);
296  maxExtents.X = Math.Max(point.X, maxExtents.X);
297  maxExtents.Y = Math.Max(point.Y, maxExtents.Y);
298  }
299  }
300  }
301 
302  private List<Vector2> GenerateConvexHull()
303  {
304  List<Structure> subWalls = Structure.WallList.FindAll(wall => wall.Submarine == submarine);
305 
306  if (subWalls.Count == 0)
307  {
308  return new List<Vector2> { new Vector2(-1.0f, 1.0f), new Vector2(1.0f, 1.0f), new Vector2(0.0f, -1.0f) };
309  }
310 
311  List<Vector2> points = new List<Vector2>();
312 
313  foreach (Structure wall in subWalls)
314  {
315  points.Add(new Vector2(wall.Rect.X, wall.Rect.Y));
316  points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y));
317  points.Add(new Vector2(wall.Rect.X, wall.Rect.Y - wall.Rect.Height));
318  points.Add(new Vector2(wall.Rect.X + wall.Rect.Width, wall.Rect.Y - wall.Rect.Height));
319  }
320 
321  List<Vector2> hullPoints = MathUtils.GiftWrap(points);
322 
323  return hullPoints;
324  }
325 
326  public void Update(float deltaTime)
327  {
328  while (impactQueue.Count > 0)
329  {
330  var impact = impactQueue.Dequeue();
331 
332  if (impact.Target.UserData is VoronoiCell cell)
333  {
334  HandleLevelCollision(impact, cell);
335  }
336  else if (impact.Target.Body.UserData is Structure)
337  {
338  HandleLevelCollision(impact);
339  }
340  else if (impact.Target.Body.UserData is Submarine otherSub)
341  {
342  HandleSubCollision(impact, otherSub);
343  }
344  else if (impact.Target.Body.UserData is Limb limb)
345  {
346  HandleLimbCollision(impact, limb);
347  }
348  }
349 
350  //-------------------------
351 
352  if (Body.FarseerBody.BodyType == BodyType.Static) { return; }
353 
354  ClientUpdatePosition(deltaTime);
355  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
356 
357  Vector2 totalForce = CalculateBuoyancy();
358 
359  //-------------------------
360 
361  //if outside left or right edge of the level
362  if (Level.Loaded != null && (Position.X < 0 || Position.X > Level.Loaded.Size.X))
363  {
364  Rectangle worldBorders = Borders;
365  worldBorders.Location += MathUtils.ToPoint(Position);
366 
367  //push the sub back below the upper "barrier" of the level
368  if (worldBorders.Y > Level.Loaded.Size.Y)
369  {
370  Body.LinearVelocity = new Vector2(
372  Math.Min(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.Size.Y - worldBorders.Y)));
373  }
374  else if (worldBorders.Y - worldBorders.Height < Level.Loaded.BottomPos)
375  {
376  Body.LinearVelocity = new Vector2(
378  Math.Max(Body.LinearVelocity.Y, ConvertUnits.ToSimUnits(Level.Loaded.BottomPos - (worldBorders.Y - worldBorders.Height))));
379  }
380 
381  float distance = Position.X < -Level.OutsideBoundsCurrentMargin ?
382  Math.Abs(Position.X + Level.OutsideBoundsCurrentMargin) :
384  if (distance > 0)
385  {
386  if (distance > Level.OutsideBoundsCurrentHardLimit)
387  {
388  if (Position.X < 0)
389  {
390  Body.LinearVelocity = new Vector2(Math.Max(0, Body.LinearVelocity.X), Body.LinearVelocity.Y);
391  }
392  else
393  {
394  Body.LinearVelocity = new Vector2(Math.Min(0, Body.LinearVelocity.X), Body.LinearVelocity.Y);
395  }
396  }
398  {
399  distance += (float)Math.Pow((distance - Level.OutsideBoundsCurrentMarginExponential) * 0.01f, 2.0f);
400  }
401  float force = distance * 0.5f;
402  totalForce += (Position.X < 0 ? Vector2.UnitX : -Vector2.UnitX) * force;
403  if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
404  {
405  GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(force * 0.0001f, 5.0f));
406  }
407  }
408  }
409 
410  //-------------------------
411 
412  //if heading up and there's another sub on top of us, gradually force it upwards
413  //(i.e. apply "artificial buoyancy" to it) to prevent us from getting pinned under it
414  //only applies to enemy subs with no enemies inside them (like destroyed pirate subs)
415  if (totalForce.Y > 0)
416  {
417  ContactEdge contactEdge = Body?.FarseerBody?.ContactList;
418  while (contactEdge?.Contact != null)
419  {
420  if (contactEdge.Contact.Enabled &&
421  contactEdge.Other.UserData is Submarine otherSubmarine &&
422  otherSubmarine.TeamID != Submarine.TeamID &&
423  contactEdge.Contact.IsTouching)
424  {
425  contactEdge.Contact.GetWorldManifold(out Vector2 _, out FixedArray2<Vector2> points);
426  if (points[0].Y > Body.SimPosition.Y &&
427  !Character.CharacterList.Any(c => c.Submarine == otherSubmarine && !c.IsIncapacitated && c.TeamID == otherSubmarine.TeamID))
428  {
429  otherSubmarine.GetConnectedSubs().ForEach(s => s.SubBody.forceUpwardsTimer += deltaTime);
430  break;
431  }
432  }
433  contactEdge = contactEdge.Next;
434  }
435  }
436 
437  //-------------------------
438 
439  if (Body.LinearVelocity.LengthSquared() > 0.0001f)
440  {
441  //TODO: sync current drag with clients?
442  float attachedMass = 0.0f;
443  JointEdge jointEdge = Body.FarseerBody.JointList;
444  while (jointEdge != null)
445  {
446  Body otherBody = jointEdge.Joint.BodyA == Body.FarseerBody ? jointEdge.Joint.BodyB : jointEdge.Joint.BodyA;
447  Character character = (otherBody.UserData as Limb)?.character;
448  if (character != null) attachedMass += character.Mass;
449 
450  jointEdge = jointEdge.Next;
451  }
452 
453  float horizontalDragCoefficient = MathHelper.Clamp(HorizontalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
454  totalForce.X -= Math.Sign(Body.LinearVelocity.X) * Body.LinearVelocity.X * Body.LinearVelocity.X * horizontalDragCoefficient * Body.Mass;
455 
456  float verticalDragCoefficient = MathHelper.Clamp(VerticalDrag + attachedMass / 5000.0f, 0.0f, MaxDrag);
457  totalForce.Y -= Math.Sign(Body.LinearVelocity.Y) * Body.LinearVelocity.Y * Body.LinearVelocity.Y * verticalDragCoefficient * Body.Mass;
458  }
459 
460  ApplyForce(totalForce);
461 
462  if (Velocity.LengthSquared() < 0.01f)
463  {
464  levelContacts.Clear();
465  levelContacts.AddRange(GetLevelContacts(Body));
466  for (int i = 0; i < levelContacts.Count; i++)
467  {
468  for (int j = i + 1; j < levelContacts.Count; j++)
469  {
470  levelContacts[i].GetWorldManifold(out Vector2 normal1, out _);
471  levelContacts[j].GetWorldManifold(out Vector2 normal2, out _);
472 
473  //normals pointing in different directions = sub lodged between two walls
474  if (Vector2.Dot(normal1, normal2) < 0)
475  {
476  //apply an extra force to hopefully dislodge the sub
477  ApplyForce(totalForce * 100.0f);
478  i = levelContacts.Count;
479  break;
480  }
481  }
482  }
483  }
484 
485  UpdateDepthDamage(deltaTime);
486 
487  forceUpwardsTimer = MathHelper.Clamp(forceUpwardsTimer - deltaTime * 0.1f, 0.0f, ForceUpwardsDelay);
488  }
489 
490  partial void ClientUpdatePosition(float deltaTime);
491 
497  private void DisplaceCharacters(Vector2 subTranslation)
498  {
499  Rectangle worldBorders = Borders;
500  worldBorders.Location += MathUtils.ToPoint(ConvertUnits.ToDisplayUnits(Body.SimPosition));
501 
502  Vector2 translateDir = Vector2.Normalize(subTranslation);
503  if (!MathUtils.IsValid(translateDir)) translateDir = Vector2.UnitY;
504 
505  foreach (Character c in Character.CharacterList)
506  {
508  {
509  continue;
510  }
511 
512  foreach (Limb limb in c.AnimController.Limbs)
513  {
514  if (limb.IsSevered) { continue; }
515  //if the character isn't inside the bounding box, continue
516  if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) { continue; }
517 
518  //cast a line from the position of the character to the same direction as the translation of the sub
519  //and see where it intersects with the bounding box
520  if (!MathUtils.GetLineRectangleIntersection(limb.WorldPosition,
521  limb.WorldPosition + translateDir * 100000.0f, worldBorders, out Vector2 intersection))
522  {
523  //should never happen when casting a line out from inside the bounding box
524  Debug.Assert(false);
525  continue;
526  }
527 
528 
529  //"+ translatedir" in order to move the character slightly away from the wall
530  c.AnimController.SetPosition(ConvertUnits.ToSimUnits(c.WorldPosition + (intersection - limb.WorldPosition)) + translateDir);
531 
532  return;
533  }
534 
535  }
536  }
537 
538  private Vector2 CalculateBuoyancy()
539  {
540  if (Submarine.LockY) { return Vector2.Zero; }
541 
542  //calculate the buoyancy for all connected subs
543  //doing it separately for each connected sub means e.g. a flooded drone barely
544  //affects the buoyancy of the main sub even if there was as much water in the
545  //drone as the whole ballast volume of the sub
546  var connectedSubs = submarine.GetConnectedSubs();
547  float waterVolume = 0.0f;
548  float volume = 0.0f;
549  float totalMass = connectedSubs.Sum(s => s.SubBody.Body.Mass);
550  foreach (Hull hull in Hull.HullList)
551  {
552  if (hull.Submarine == null || !connectedSubs.Contains(hull.Submarine)) { continue; }
553  if (hull.Submarine.PhysicsBody is not { BodyType: BodyType.Dynamic }) { continue; }
554  waterVolume += hull.WaterVolume;
555  volume += hull.Volume;
556  }
557 
558  float waterPercentage = volume <= 0.0f ? 0.0f : waterVolume / volume;
559  float buoyancy = NeutralBallastPercentage - waterPercentage;
560  float massRatio = Body.Mass / totalMass;
561  if (buoyancy > 0.0f)
562  {
563  buoyancy *= 2.0f;
564  }
565  else
566  {
567  buoyancy = Math.Max(buoyancy, -0.5f);
568  }
569  if (forceUpwardsTimer > 0.0f)
570  {
571  buoyancy = MathHelper.Lerp(buoyancy, 0.1f, forceUpwardsTimer / ForceUpwardsDelay);
572  }
573  return new Vector2(0.0f, buoyancy * totalMass * 10.0f) * massRatio;
574  }
575 
576  public void ApplyForce(Vector2 force)
577  {
578  Body.ApplyForce(force);
579  }
580 
581  public void SetPosition(Vector2 position)
582  {
583  Body.SetTransform(ConvertUnits.ToSimUnits(position), 0.0f);
584  }
585 
586  private void UpdateDepthDamage(float deltaTime)
587  {
588  if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
589  if (Level.Loaded == null) { return; }
590 
591  //camera shake and sounds start playing 500 meters before crush depth
592  const float CosmeticEffectThreshold = -500.0f;
593  //breaches won't get any more severe 500 meters below crush depth
594  const float MaxEffectThreshold = 500.0f;
595  const float MinWallDamageProbability = 0.1f;
596  const float MaxWallDamageProbability = 1.0f;
597  const float MinWallDamage = 50f;
598  const float MaxWallDamage = 500.0f;
599  const float MinCameraShake = 10f;
600  const float MaxCameraShake = 50.0f;
601  //delay at the start of the round during which you take no depth damage
602  //(gives you a bit of time to react and return if you start the round in a level that's too deep)
603  const float MinRoundDuration = 60.0f;
604 
605  if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
606  {
607  return;
608  }
609 
610  damageSoundTimer -= deltaTime;
611  if (damageSoundTimer <= 0.0f)
612  {
613  const float PressureSoundRange = -CosmeticEffectThreshold;
614  //Ratio between 0 (where the 'approaching crush depth' indication starts) and 1 (at crush depth or past it)
615  float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)) / PressureSoundRange, 0f, 1f);
616 #if CLIENT
617  SoundPlayer.PlayDamageSound("pressure", MathHelper.Lerp(0f, 100f, closenessToCrushDepthRatio), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f, gain: 1f + closenessToCrushDepthRatio * 2);
618 #endif
619  damageSoundTimer = Rand.Range(5.0f, 10.0f);
620  }
621 
622  depthDamageTimer -= deltaTime;
623  if (depthDamageTimer <= 0.0f && (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > MinRoundDuration))
624  {
625  foreach (Structure wall in Structure.WallList)
626  {
627  if (wall.Submarine != submarine) { continue; }
628 
629  float wallCrushDepth = wall.CrushDepth;
630  float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
631  float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f);
632 
633  if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; }
634 
635  float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio);
636  if (pastCrushDepth > 0)
637  {
638  Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f);
639 #if CLIENT
640  SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f);
641 #endif
642  }
643  if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
644  {
645  GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio));
646  }
647  }
648  depthDamageTimer = Rand.Range(5.0f, 10.0f);
649  }
650  }
651 
652  public void FlipX()
653  {
654  List<Vector2> convexHull = GenerateConvexHull();
655  for (int i = 0; i < convexHull.Count; i++)
656  {
657  convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
658  }
659  HullVertices = convexHull;
660  }
661 
662  public bool OnCollision(Fixture f1, Fixture f2, Contact contact)
663  {
664  if (f2.Body.UserData is Limb limb)
665  {
666  bool collision = CheckCharacterCollision(contact, limb.character);
667  if (collision)
668  {
669  lock (impactQueue)
670  {
671  impactQueue.Enqueue(new Impact(f1, f2, contact));
672  }
673  }
674  return collision;
675  }
676  else if (f2.Body.UserData is Character character)
677  {
678  return CheckCharacterCollision(contact, character);
679  }
680  else if (f1.UserData is Items.Components.DockingPort || f2.UserData is Items.Components.DockingPort)
681  {
682  return false;
683  }
684 
685  lock (impactQueue)
686  {
687  impactQueue.Enqueue(new Impact(f1, f2, contact));
688  }
689  return true;
690  }
691 
692  private bool CheckCharacterCollision(Contact contact, Character character)
693  {
694  if (character.Submarine != null) { return false; }
695  switch (character.AnimController.CanEnterSubmarine)
696  {
697  case CanEnterSubmarine.False:
698  //characters that can't enter the sub always collide regardless of gaps
699  return true;
700  case CanEnterSubmarine.Partial:
701  //characters that can partially enter the sub can poke their limbs inside, but not the collider
702  if (contact.FixtureB.Body ==
704  {
705  return true;
706  }
707  if (contact.FixtureB.Body.UserData is Limb limb &&
708  !limb.Params.CanEnterSubmarine)
709  {
710  return true;
711  }
712  break;
713  }
714 
715  contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
716 
717  Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ?
718  Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity);
719 
720  //try to find the hull right next to the contact point
721  Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal * 0.1f);
722  Hull newHull = Hull.FindHull(targetPos, null);
723  //not found, try searching a bit further
724  if (newHull == null)
725  {
726  targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal);
727  newHull = Hull.FindHull(targetPos, null);
728  }
729  //still not found, try searching in the direction the character is heading to
730  if (newHull == null)
731  {
732  targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel);
733  newHull = Hull.FindHull(targetPos, null);
734  }
735 
736  //if all the bodies of a wall have been disabled, we don't need to care about gaps (can always pass through)
737  if (contact.FixtureA.UserData is not Structure wall || !wall.AllSectionBodiesDisabled())
738  {
739  var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine);
740  Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f);
741  if (adjacentGap == null) { return true; }
742  }
743 
744  if (character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial)
745  {
746  return contact.FixtureB.Body == character.AnimController.Collider.FarseerBody;
747  }
748  else
749  {
750  if (newHull != null)
751  {
752  CoroutineManager.Invoke(() =>
753  {
754  if (character != null && !character.Removed)
755  {
756  character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true);
757  }
758  });
759  }
760  }
761  return false;
762  }
763 
764  private void HandleLimbCollision(Impact collision, Limb limb)
765  {
766  if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
767 
768  float impactMass = limb.Mass;
769  var enemyAI = limb.character.AIController as EnemyAIController;
770  float attackMultiplier = 1.0f;
771  if (enemyAI?.ActiveAttack != null)
772  {
773  impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass);
774  attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier;
775  }
776 
777  if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static)
778  {
779  Vector2 normal =
780  Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
781  Vector2.UnitY :
782  Vector2.Normalize(Body.SimPosition - limb.SimPosition);
783 
784  float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1);
785  impact *= attackMultiplier;
786 
787  ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
788  foreach (Submarine dockedSub in submarine.DockedTo)
789  {
790  dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
791  }
792  }
793 
794  //find all contacts between the limb and level walls
795  IEnumerable<Contact> levelContacts = GetLevelContacts(limb.body);
796  int levelContactCount = levelContacts.Count();
797 
798  if (levelContactCount == 0) { return; }
799 
800  //if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it
801  //not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete),
802  //but more realistic than bouncing and prevents using characters as "bumpers" that prevent all collision damage
803  Vector2 avgContactNormal = Vector2.Zero;
804  foreach (Contact levelContact in levelContacts)
805  {
806  levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
807 
808  //if the contact normal is pointing from the limb towards the level cell it's touching, flip the normal
809  VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
810  ((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
811 
812  var cellDiff = ConvertUnits.ToDisplayUnits(limb.body.SimPosition) - cell.Center;
813  if (Vector2.Dot(contactNormal, cellDiff) < 0)
814  {
815  contactNormal = -contactNormal;
816  }
817 
818  avgContactNormal += contactNormal;
819 
820  //apply impacts at the positions where this sub is touching the limb
821  ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false);
822  }
823  avgContactNormal /= levelContactCount;
824 
825  float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal);
826  if (contactDot > 0.001f)
827  {
828  Vector2 velChange = Vector2.Normalize(Body.LinearVelocity) * contactDot;
829  if (!MathUtils.IsValid(velChange))
830  {
831  GameAnalyticsManager.AddErrorEventOnce(
832  "SubmarineBody.HandleLimbCollision:" + submarine.ID,
833  GameAnalyticsManager.ErrorSeverity.Error,
834  "Invalid velocity change in SubmarineBody.HandleLimbCollision (submarine velocity: " + Body.LinearVelocity
835  + ", avgContactNormal: " + avgContactNormal
836  + ", contactDot: " + contactDot
837  + ", velChange: " + velChange + ")");
838  return;
839  }
840 
841  Body.LinearVelocity -= velChange;
842 
843  if (contactDot > 0.1f)
844  {
845  float damageAmount = contactDot * Body.Mass / limb.character.Mass;
846  limb.character.LastDamageSource = submarine;
847  limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
848  AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(),
849  stun: 0.0f,
850  playSound: true,
851  attackImpulse: Vector2.Zero);
852 
853  if (limb.character.IsDead)
854  {
855  foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
856  {
857  if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
858  limb.character.AnimController.SeverLimbJoint(limbJoint);
859  }
860  }
861  }
862  }
863  }
864 
865  private static IEnumerable<Contact> GetLevelContacts(PhysicsBody body)
866  {
867  ContactEdge contactEdge = body.FarseerBody.ContactList;
868  while (contactEdge?.Contact != null)
869  {
870  if (contactEdge.Contact.Enabled &&
871  contactEdge.Contact.IsTouching &&
872  contactEdge.Other?.UserData is VoronoiCell)
873  {
874  yield return contactEdge.Contact;
875  }
876  contactEdge = contactEdge.Next;
877  }
878  }
879 
880  private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
881  {
882  if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10)
883  {
884  //ignore level collisions for the first 10 seconds of the round in case the sub spawns in a way that causes it to hit a wall
885  //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
886  return;
887  }
888 
889  float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
890 
891  ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
892  foreach (Submarine dockedSub in submarine.DockedTo)
893  {
894  dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
895  }
896 
897  if (cell != null && cell.IsDestructible && wallImpact > 0.0f)
898  {
899  var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell));
900  if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f)
901  {
902  var damagedStructures = Explosion.RangedStructureDamage(
903  ConvertUnits.ToDisplayUnits(impact.ImpactPos),
904  500.0f,
905  hitWall.WallDamageOnTouch,
906  levelWallDamage: 0.0f);
907 #if CLIENT
908  PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash");
909 #endif
910  }
911  }
912 
913  HandleLevelCollisionProjSpecific(impact);
914  }
915 
916 
917  partial void HandleLevelCollisionProjSpecific(Impact impact);
918 
919  private void HandleSubCollision(Impact impact, Submarine otherSub)
920  {
921  Debug.Assert(otherSub != submarine);
922 
923  //submarine outside the level (despawned respawn shuttle?)
924  //no need to apply impacts between colliding subs
925  if (submarine.IsAboveLevel)
926  {
927  return;
928  }
929 
930  Vector2 normal = impact.Normal;
931  if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
932  {
933  normal = -normal;
934  }
935 
936  float thisMass = Body.Mass + submarine.DockedTo.Sum(s => s.PhysicsBody.Mass);
937  float otherMass = otherSub.PhysicsBody.Mass + otherSub.DockedTo.Sum(s => s.PhysicsBody.Mass);
938  float massRatio = otherMass / (thisMass + otherMass);
939 
940  float impulse = (Vector2.Dot(impact.Velocity, normal) / 2.0f) * massRatio;
941 
942  //apply impact to this sub (the other sub takes care of this in its own collision callback)
943  ApplyImpact(impulse, normal, impact.ImpactPos);
944  foreach (Submarine dockedSub in submarine.DockedTo)
945  {
946  dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
947  }
948 
949  //find all contacts between this sub and level walls
950  IEnumerable<Contact> levelContacts = GetLevelContacts(Body);
951  int levelContactCount = levelContacts.Count();
952  if (levelContactCount == 0) { return; }
953 
954  //if this sub is in contact with the level, apply artifical impacts
955  //to both subs to prevent the other sub from bouncing on top of this one
956  //and to fake the other sub "crushing" this one against a wall
957  Vector2 avgContactNormal = Vector2.Zero;
958  foreach (Contact levelContact in levelContacts)
959  {
960  levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
961 
962  //if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal
963  VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell;
964 
965  var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center;
966  if (Vector2.Dot(contactNormal, cellDiff) < 0)
967  {
968  contactNormal = -contactNormal;
969  }
970 
971  avgContactNormal += contactNormal;
972 
973  //apply impacts at the positions where this sub is touching the level
974  ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos);
975  }
976  avgContactNormal /= levelContactCount;
977 
978  //apply an impact to the other sub
979  float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal);
980  if (contactDot > 0.0f)
981  {
982  if (otherSub.PhysicsBody.LinearVelocity.LengthSquared() > 0.0001f)
983  {
984  otherSub.PhysicsBody.LinearVelocity -= Vector2.Normalize(otherSub.PhysicsBody.LinearVelocity) * contactDot;
985  }
986 
987  impulse = Vector2.Dot(otherSub.Velocity, normal);
988  otherSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
989  foreach (Submarine dockedSub in otherSub.DockedTo)
990  {
991  dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
992  }
993  }
994  }
995 
996  private void ApplyImpact(float impact, Vector2 direction, Vector2 impactPos, bool applyDamage = true)
997  {
998  if (impact < MinCollisionImpact) { return; }
999 
1000  Vector2 impulse = direction * impact * 0.5f;
1001  impulse = impulse.ClampLength(MaxCollisionImpact);
1002 
1003  float impulseMagnitude = impulse.Length();
1004 
1005  if (!MathUtils.IsValid(impulse))
1006  {
1007  string errorMsg =
1008  "Invalid impulse in SubmarineBody.ApplyImpact: " + impulse +
1009  ". Direction: " + direction + ", body position: " + Body.SimPosition + ", impact: " + impact + ".";
1010  if (GameMain.NetworkMember != null)
1011  {
1012  errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
1013  }
1014  if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
1015  GameAnalyticsManager.AddErrorEventOnce(
1016  "SubmarineBody.ApplyImpact:InvalidImpulse",
1017  GameAnalyticsManager.ErrorSeverity.Error,
1018  errorMsg);
1019  return;
1020  }
1021 
1022 #if CLIENT
1023  if (Character.Controlled != null && Character.Controlled.Submarine == submarine && Character.Controlled.KnockbackCooldownTimer <= 0.0f)
1024  {
1025  GameMain.GameScreen.Cam.Shake = Math.Max(impact * 10.0f, GameMain.GameScreen.Cam.Shake);
1026  if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player))
1027  {
1028  float angularVelocity =
1029  (impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y
1030  - (impactPos.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X;
1031  GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f);
1032  }
1033  }
1034 #endif
1035 
1036  foreach (Character c in Character.CharacterList)
1037  {
1038  if (c.Submarine != submarine) { continue; }
1039  if (c.KnockbackCooldownTimer > 0.0f) { continue; }
1040 
1041  c.KnockbackCooldownTimer = Character.KnockbackCooldown;
1042 
1043  foreach (Limb limb in c.AnimController.Limbs)
1044  {
1045  if (limb.IsSevered) { continue; }
1046  limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
1047  }
1048 
1049  bool holdingOntoSomething = false;
1050  if (c.SelectedSecondaryItem != null)
1051  {
1052  holdingOntoSomething = c.SelectedSecondaryItem.IsLadder ||
1053  (c.SelectedSecondaryItem.GetComponent<Controller>()?.LimbPositions.Any() ?? false);
1054  }
1055  if (!holdingOntoSomething && c.SelectedItem != null)
1056  {
1057  holdingOntoSomething = c.SelectedItem.GetComponent<Controller>()?.LimbPositions.Any() ?? false;
1058  }
1059  if (!holdingOntoSomething)
1060  {
1061  c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
1062  //stun for up to 2 second if the impact equal or higher to the maximum impact
1063  if (impact >= MaxCollisionImpact)
1064  {
1065  float impactDamage = c.AnimController.GetImpactDamage(impact);
1066  c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true);
1067  }
1068  }
1069  }
1070 
1071  foreach (Item item in Item.ItemList)
1072  {
1073  if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; }
1074  if (item.body.Mass > impulseMagnitude) { continue; }
1075 
1076  item.body.ApplyLinearImpulse(impulse, 10.0f);
1077  item.PositionUpdateInterval = 0.0f;
1078  }
1079 
1080  float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;
1081  var damagedStructures = Explosion.RangedStructureDamage(
1082  ConvertUnits.ToDisplayUnits(impactPos),
1083  impact * 50.0f,
1084  dmg, dmg);
1085 
1086 #if CLIENT
1087  PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt");
1088 #endif
1089  }
1090 
1091  public void Remove()
1092  {
1093  Body.Remove();
1094  }
1095  }
1096 }
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static World World
Definition: GameMain.cs:28
static GameScreen GameScreen
Definition: GameMain.cs:56
static NetworkMember NetworkMember
Definition: GameMain.cs:41
static GameSession GameSession
Definition: GameMain.cs:45
override Camera Cam
Definition: GameScreen.cs:27
static readonly List< Hull > HullList
override Quad2D GetTransformedQuad()
static readonly List< Item > ItemList
const float OutsideBoundsCurrentMargin
How far outside the boundaries of the level the water current that pushes subs towards the level star...
const float OutsideBoundsCurrentHardLimit
How far outside the boundaries of the level the current stops submarines entirely
const float OutsideBoundsCurrentMarginExponential
How far outside the boundaries of the level the strength of the current starts to increase exponentia...
static readonly List< MapEntity > MapEntityList
Definition: MapEntity.cs:15
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
CanEnterSubmarine CanEnterSubmarine
Definition: Ragdoll.cs:339
PhysicsBody? Collider
Definition: Ragdoll.cs:145
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
Definition: Ragdoll.cs:1882
Rectangle VisibleBorders
Extents of all the visible items/structures/hulls (including ones without a physics body)
List< PosInfo > PositionBuffer
Rectangle Borders
Extents of the solid items/structures (ones with a physics body) and hulls
bool OnCollision(Fixture f1, Fixture f2, Contact contact)
void ApplyForce(Vector2 force)
SubmarineBody(Submarine sub, bool showErrorMessages=true)
readonly PhysicsBody Body
void SetPosition(Vector2 position)
const Category CollidesWith
const float NeutralBallastPercentage
void Update(float deltaTime)
List< Vector2 > HullVertices
static bool RectContains(Rectangle rect, Vector2 pos, bool inclusive=false)
float? RealWorldDepth
How deep down the sub is from the surface of Europa in meters (affected by level type,...
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!