Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/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 
86  {
87  get;
88  private set;
89  }
90 
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 CLIENT
589  if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
590 #endif
591  if (Level.Loaded == null) { return; }
592 
593  //camera shake and sounds start playing 500 meters before crush depth
594  const float CosmeticEffectThreshold = -500.0f;
595  //breaches won't get any more severe 500 meters below crush depth
596  const float MaxEffectThreshold = 500.0f;
597  const float MinWallDamageProbability = 0.1f;
598  const float MaxWallDamageProbability = 1.0f;
599  const float MinWallDamage = 50f;
600  const float MaxWallDamage = 500.0f;
601  const float MinCameraShake = 5f;
602  const float MaxCameraShake = 50.0f;
603  //delay at the start of the round during which you take no depth damage
604  //(gives you a bit of time to react and return if you start the round in a level that's too deep)
605  const float MinRoundDuration = 60.0f;
606 
607  if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
608  {
609  return;
610  }
611 
612  damageSoundTimer -= deltaTime;
613  if (damageSoundTimer <= 0.0f)
614  {
615 #if CLIENT
616  SoundPlayer.PlayDamageSound("pressure", Rand.Range(0.0f, 100.0f), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f);
617 #endif
618  damageSoundTimer = Rand.Range(5.0f, 10.0f);
619  }
620 
621  depthDamageTimer -= deltaTime;
622  if (depthDamageTimer <= 0.0f && (GameMain.GameSession == null || GameMain.GameSession.RoundDuration > MinRoundDuration))
623  {
624  foreach (Structure wall in Structure.WallList)
625  {
626  if (wall.Submarine != submarine) { continue; }
627 
628  float wallCrushDepth = wall.CrushDepth;
629  float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
630  float pastCrushDepthRatio = Math.Clamp(pastCrushDepth / MaxEffectThreshold, 0.0f, 1.0f);
631 
632  if (Rand.Range(0.0f, 1.0f) > MathHelper.Lerp(MinWallDamageProbability, MaxWallDamageProbability, pastCrushDepthRatio)) { continue; }
633 
634  float damage = MathHelper.Lerp(MinWallDamage, MaxWallDamage, pastCrushDepthRatio);
635  if (pastCrushDepth > 0)
636  {
637  Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, damage, levelWallDamage: 0.0f);
638 #if CLIENT
639  SoundPlayer.PlayDamageSound("StructureBlunt", Rand.Range(0.0f, 100.0f), wall.WorldPosition, 2000.0f);
640 #endif
641  }
642  if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
643  {
644  GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, MathHelper.Lerp(MinCameraShake, MaxCameraShake, pastCrushDepthRatio));
645  }
646  }
647  depthDamageTimer = Rand.Range(5.0f, 10.0f);
648  }
649  }
650 
651  public void FlipX()
652  {
653  List<Vector2> convexHull = GenerateConvexHull();
654  for (int i = 0; i < convexHull.Count; i++)
655  {
656  convexHull[i] = ConvertUnits.ToSimUnits(convexHull[i]);
657  }
658  HullVertices = convexHull;
659  }
660 
661  public bool OnCollision(Fixture f1, Fixture f2, Contact contact)
662  {
663  if (f2.Body.UserData is Limb limb)
664  {
665  bool collision = CheckCharacterCollision(contact, limb.character);
666  if (collision)
667  {
668  lock (impactQueue)
669  {
670  impactQueue.Enqueue(new Impact(f1, f2, contact));
671  }
672  }
673  return collision;
674  }
675  else if (f2.Body.UserData is Character character)
676  {
677  return CheckCharacterCollision(contact, character);
678  }
679  else if (f1.UserData is Items.Components.DockingPort || f2.UserData is Items.Components.DockingPort)
680  {
681  return false;
682  }
683 
684  lock (impactQueue)
685  {
686  impactQueue.Enqueue(new Impact(f1, f2, contact));
687  }
688  return true;
689  }
690 
691  private bool CheckCharacterCollision(Contact contact, Character character)
692  {
693  if (character.Submarine != null) { return false; }
694  switch (character.AnimController.CanEnterSubmarine)
695  {
696  case CanEnterSubmarine.False:
697  //characters that can't enter the sub always collide regardless of gaps
698  return true;
699  case CanEnterSubmarine.Partial:
700  //characters that can partially enter the sub can poke their limbs inside, but not the collider
701  if (contact.FixtureB.Body ==
703  {
704  return true;
705  }
706  if (contact.FixtureB.Body.UserData is Limb limb &&
707  !limb.Params.CanEnterSubmarine)
708  {
709  return true;
710  }
711  break;
712  }
713 
714  contact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> points);
715 
716  Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ?
717  Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity);
718 
719  //try to find the hull right next to the contact point
720  Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal * 0.1f);
721  Hull newHull = Hull.FindHull(targetPos, null);
722  //not found, try searching a bit further
723  if (newHull == null)
724  {
725  targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal);
726  newHull = Hull.FindHull(targetPos, null);
727  }
728  //still not found, try searching in the direction the character is heading to
729  if (newHull == null)
730  {
731  targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel);
732  newHull = Hull.FindHull(targetPos, null);
733  }
734 
735  //if all the bodies of a wall have been disabled, we don't need to care about gaps (can always pass through)
736  if (contact.FixtureA.UserData is not Structure wall || !wall.AllSectionBodiesDisabled())
737  {
738  var gaps = newHull?.ConnectedGaps ?? Gap.GapList.Where(g => g.Submarine == submarine);
739  Gap adjacentGap = Gap.FindAdjacent(gaps, ConvertUnits.ToDisplayUnits(points[0]), 200.0f);
740  if (adjacentGap == null) { return true; }
741  }
742 
743  if (character.AnimController.CanEnterSubmarine == CanEnterSubmarine.Partial)
744  {
745  return contact.FixtureB.Body == character.AnimController.Collider.FarseerBody;
746  }
747  else
748  {
749  if (newHull != null)
750  {
751  CoroutineManager.Invoke(() =>
752  {
753  if (character != null && !character.Removed)
754  {
755  character.AnimController.FindHull(newHull.WorldPosition, setSubmarine: true);
756  }
757  });
758  }
759  }
760  return false;
761  }
762 
763  private void HandleLimbCollision(Impact collision, Limb limb)
764  {
765  if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
766 
767  float impactMass = limb.Mass;
768  var enemyAI = limb.character.AIController as EnemyAIController;
769  float attackMultiplier = 1.0f;
770  if (enemyAI?.ActiveAttack != null)
771  {
772  impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass);
773  attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier;
774  }
775 
776  if (impactMass * attackMultiplier > MinImpactLimbMass && Body.BodyType != BodyType.Static)
777  {
778  Vector2 normal =
779  Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
780  Vector2.UnitY :
781  Vector2.Normalize(Body.SimPosition - limb.SimPosition);
782 
783  float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1);
784  impact *= attackMultiplier;
785 
786  ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
787  foreach (Submarine dockedSub in submarine.DockedTo)
788  {
789  dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
790  }
791  }
792 
793  //find all contacts between the limb and level walls
794  IEnumerable<Contact> levelContacts = GetLevelContacts(limb.body);
795  int levelContactCount = levelContacts.Count();
796 
797  if (levelContactCount == 0) { return; }
798 
799  //if the limb is in contact with the level, apply an artifical impact to prevent the sub from bouncing on top of it
800  //not a very realistic way to handle the collisions (makes it seem as if the characters were made of reinforced concrete),
801  //but more realistic than bouncing and prevents using characters as "bumpers" that prevent all collision damage
802  Vector2 avgContactNormal = Vector2.Zero;
803  foreach (Contact levelContact in levelContacts)
804  {
805  levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
806 
807  //if the contact normal is pointing from the limb towards the level cell it's touching, flip the normal
808  VoronoiCell cell = levelContact.FixtureB.UserData is VoronoiCell ?
809  ((VoronoiCell)levelContact.FixtureB.UserData) : ((VoronoiCell)levelContact.FixtureA.UserData);
810 
811  var cellDiff = ConvertUnits.ToDisplayUnits(limb.body.SimPosition) - cell.Center;
812  if (Vector2.Dot(contactNormal, cellDiff) < 0)
813  {
814  contactNormal = -contactNormal;
815  }
816 
817  avgContactNormal += contactNormal;
818 
819  //apply impacts at the positions where this sub is touching the limb
820  ApplyImpact((Vector2.Dot(-collision.Velocity, contactNormal) / 2.0f) / levelContactCount, contactNormal, collision.ImpactPos, applyDamage: false);
821  }
822  avgContactNormal /= levelContactCount;
823 
824  float contactDot = Vector2.Dot(Body.LinearVelocity, -avgContactNormal);
825  if (contactDot > 0.001f)
826  {
827  Vector2 velChange = Vector2.Normalize(Body.LinearVelocity) * contactDot;
828  if (!MathUtils.IsValid(velChange))
829  {
830  GameAnalyticsManager.AddErrorEventOnce(
831  "SubmarineBody.HandleLimbCollision:" + submarine.ID,
832  GameAnalyticsManager.ErrorSeverity.Error,
833  "Invalid velocity change in SubmarineBody.HandleLimbCollision (submarine velocity: " + Body.LinearVelocity
834  + ", avgContactNormal: " + avgContactNormal
835  + ", contactDot: " + contactDot
836  + ", velChange: " + velChange + ")");
837  return;
838  }
839 
840  Body.LinearVelocity -= velChange;
841 
842  if (contactDot > 0.1f)
843  {
844  float damageAmount = contactDot * Body.Mass / limb.character.Mass;
845  limb.character.LastDamageSource = submarine;
846  limb.character.DamageLimb(ConvertUnits.ToDisplayUnits(collision.ImpactPos), limb,
847  AfflictionPrefab.ImpactDamage.Instantiate(damageAmount).ToEnumerable(),
848  stun: 0.0f,
849  playSound: true,
850  attackImpulse: Vector2.Zero);
851 
852  if (limb.character.IsDead)
853  {
854  foreach (LimbJoint limbJoint in limb.character.AnimController.LimbJoints)
855  {
856  if (limbJoint.IsSevered || (limbJoint.LimbA != limb && limbJoint.LimbB != limb)) continue;
857  limb.character.AnimController.SeverLimbJoint(limbJoint);
858  }
859  }
860  }
861  }
862  }
863 
864  private static IEnumerable<Contact> GetLevelContacts(PhysicsBody body)
865  {
866  ContactEdge contactEdge = body.FarseerBody.ContactList;
867  while (contactEdge?.Contact != null)
868  {
869  if (contactEdge.Contact.Enabled &&
870  contactEdge.Contact.IsTouching &&
871  contactEdge.Other?.UserData is VoronoiCell)
872  {
873  yield return contactEdge.Contact;
874  }
875  contactEdge = contactEdge.Next;
876  }
877  }
878 
879  private void HandleLevelCollision(Impact impact, VoronoiCell cell = null)
880  {
881  if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 10)
882  {
883  //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
884  //(e.g. level without outposts to dock to and an incorrectly configured ballast that makes the sub go up)
885  return;
886  }
887 
888  float wallImpact = Vector2.Dot(impact.Velocity, -impact.Normal);
889 
890  ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
891  foreach (Submarine dockedSub in submarine.DockedTo)
892  {
893  dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
894  }
895 
896  if (cell != null && cell.IsDestructible && wallImpact > 0.0f)
897  {
898  var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell));
899  if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f)
900  {
901  var damagedStructures = Explosion.RangedStructureDamage(
902  ConvertUnits.ToDisplayUnits(impact.ImpactPos),
903  500.0f,
904  hitWall.WallDamageOnTouch,
905  levelWallDamage: 0.0f);
906 #if CLIENT
907  PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash");
908 #endif
909  }
910  }
911 
912  HandleLevelCollisionProjSpecific(impact);
913  }
914 
915 
916  partial void HandleLevelCollisionProjSpecific(Impact impact);
917 
918  private void HandleSubCollision(Impact impact, Submarine otherSub)
919  {
920  Debug.Assert(otherSub != submarine);
921 
922  Vector2 normal = impact.Normal;
923  if (impact.Target.Body == otherSub.SubBody.Body.FarseerBody)
924  {
925  normal = -normal;
926  }
927 
928  float thisMass = Body.Mass + submarine.DockedTo.Sum(s => s.PhysicsBody.Mass);
929  float otherMass = otherSub.PhysicsBody.Mass + otherSub.DockedTo.Sum(s => s.PhysicsBody.Mass);
930  float massRatio = otherMass / (thisMass + otherMass);
931 
932  float impulse = (Vector2.Dot(impact.Velocity, normal) / 2.0f) * massRatio;
933 
934  //apply impact to this sub (the other sub takes care of this in its own collision callback)
935  ApplyImpact(impulse, normal, impact.ImpactPos);
936  foreach (Submarine dockedSub in submarine.DockedTo)
937  {
938  dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
939  }
940 
941  //find all contacts between this sub and level walls
942  IEnumerable<Contact> levelContacts = GetLevelContacts(Body);
943  int levelContactCount = levelContacts.Count();
944  if (levelContactCount == 0) { return; }
945 
946  //if this sub is in contact with the level, apply artifical impacts
947  //to both subs to prevent the other sub from bouncing on top of this one
948  //and to fake the other sub "crushing" this one against a wall
949  Vector2 avgContactNormal = Vector2.Zero;
950  foreach (Contact levelContact in levelContacts)
951  {
952  levelContact.GetWorldManifold(out Vector2 contactNormal, out FixedArray2<Vector2> temp);
953 
954  //if the contact normal is pointing from the sub towards the level cell we collided with, flip the normal
955  VoronoiCell cell = levelContact.FixtureB.UserData as VoronoiCell ?? levelContact.FixtureA.UserData as VoronoiCell;
956 
957  var cellDiff = ConvertUnits.ToDisplayUnits(Body.SimPosition) - cell.Center;
958  if (Vector2.Dot(contactNormal, cellDiff) < 0)
959  {
960  contactNormal = -contactNormal;
961  }
962 
963  avgContactNormal += contactNormal;
964 
965  //apply impacts at the positions where this sub is touching the level
966  ApplyImpact((Vector2.Dot(impact.Velocity, contactNormal) / 2.0f) * massRatio / levelContactCount, contactNormal, impact.ImpactPos);
967  }
968  avgContactNormal /= levelContactCount;
969 
970  //apply an impact to the other sub
971  float contactDot = Vector2.Dot(otherSub.PhysicsBody.LinearVelocity, -avgContactNormal);
972  if (contactDot > 0.0f)
973  {
974  if (otherSub.PhysicsBody.LinearVelocity.LengthSquared() > 0.0001f)
975  {
976  otherSub.PhysicsBody.LinearVelocity -= Vector2.Normalize(otherSub.PhysicsBody.LinearVelocity) * contactDot;
977  }
978 
979  impulse = Vector2.Dot(otherSub.Velocity, normal);
980  otherSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
981  foreach (Submarine dockedSub in otherSub.DockedTo)
982  {
983  dockedSub.SubBody.ApplyImpact(impulse, normal, impact.ImpactPos);
984  }
985  }
986  }
987 
988  private void ApplyImpact(float impact, Vector2 direction, Vector2 impactPos, bool applyDamage = true)
989  {
990  if (impact < MinCollisionImpact) { return; }
991 
992  Vector2 impulse = direction * impact * 0.5f;
993  impulse = impulse.ClampLength(MaxCollisionImpact);
994 
995  float impulseMagnitude = impulse.Length();
996 
997  if (!MathUtils.IsValid(impulse))
998  {
999  string errorMsg =
1000  "Invalid impulse in SubmarineBody.ApplyImpact: " + impulse +
1001  ". Direction: " + direction + ", body position: " + Body.SimPosition + ", impact: " + impact + ".";
1002  if (GameMain.NetworkMember != null)
1003  {
1004  errorMsg += GameMain.NetworkMember.IsClient ? " Playing as a client." : " Hosting a server.";
1005  }
1006  if (GameSettings.CurrentConfig.VerboseLogging) DebugConsole.ThrowError(errorMsg);
1007  GameAnalyticsManager.AddErrorEventOnce(
1008  "SubmarineBody.ApplyImpact:InvalidImpulse",
1009  GameAnalyticsManager.ErrorSeverity.Error,
1010  errorMsg);
1011  return;
1012  }
1013 
1014 #if CLIENT
1015  if (Character.Controlled != null && Character.Controlled.Submarine == submarine && Character.Controlled.KnockbackCooldownTimer <= 0.0f)
1016  {
1017  GameMain.GameScreen.Cam.Shake = Math.Max(impact * 10.0f, GameMain.GameScreen.Cam.Shake);
1018  if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player))
1019  {
1020  float angularVelocity =
1021  (impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y
1022  - (impactPos.Y - Body.SimPosition.Y) / ConvertUnits.ToSimUnits(submarine.Borders.Height / 2) * impulse.X;
1023  GameMain.GameScreen.Cam.AngularVelocity = MathHelper.Clamp(angularVelocity * 0.1f, -1.0f, 1.0f);
1024  }
1025  }
1026 #endif
1027 
1028  foreach (Character c in Character.CharacterList)
1029  {
1030  if (c.Submarine != submarine) { continue; }
1031  if (c.KnockbackCooldownTimer > 0.0f) { continue; }
1032 
1033  c.KnockbackCooldownTimer = Character.KnockbackCooldown;
1034 
1035  foreach (Limb limb in c.AnimController.Limbs)
1036  {
1037  if (limb.IsSevered) { continue; }
1038  limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
1039  }
1040 
1041  bool holdingOntoSomething = false;
1042  if (c.SelectedSecondaryItem != null)
1043  {
1044  holdingOntoSomething = c.SelectedSecondaryItem.IsLadder ||
1045  (c.SelectedSecondaryItem.GetComponent<Controller>()?.LimbPositions.Any() ?? false);
1046  }
1047  if (!holdingOntoSomething && c.SelectedItem != null)
1048  {
1049  holdingOntoSomething = c.SelectedItem.GetComponent<Controller>()?.LimbPositions.Any() ?? false;
1050  }
1051  if (!holdingOntoSomething)
1052  {
1053  c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
1054  //stun for up to 2 second if the impact equal or higher to the maximum impact
1055  if (impact >= MaxCollisionImpact)
1056  {
1057  float impactDamage = c.AnimController.GetImpactDamage(impact);
1058  c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(impactDamage).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true);
1059  }
1060  }
1061  }
1062 
1063  foreach (Item item in Item.ItemList)
1064  {
1065  if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; }
1066  if (item.body.Mass > impulseMagnitude) { continue; }
1067 
1068  item.body.ApplyLinearImpulse(impulse, 10.0f);
1069  item.PositionUpdateInterval = 0.0f;
1070  }
1071 
1072  float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;
1073  var damagedStructures = Explosion.RangedStructureDamage(
1074  ConvertUnits.ToDisplayUnits(impactPos),
1075  impact * 50.0f,
1076  dmg, dmg);
1077 
1078 #if CLIENT
1079  PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt");
1080 #endif
1081  }
1082 
1083  public void Remove()
1084  {
1085  Body.Remove();
1086  }
1087  }
1088 }
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static GameSession?? GameSession
Definition: GameMain.cs:88
static World World
Definition: GameMain.cs:105
static GameScreen GameScreen
Definition: GameMain.cs:52
static NetworkMember NetworkMember
Definition: GameMain.cs:190
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
void ApplyForce(Vector2 force, float maxVelocity=NetConfig.MaxPhysicsBodyVelocity)
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
void SetPosition(Vector2 simPosition, bool lerp=false, bool ignorePlatforms=true, bool forceMainLimbToCollider=false, bool moveLatchers=true)
Rectangle VisibleBorders
Extents of all the visible items/structures/hulls (including ones without a physics body)
Rectangle Borders
Extents of the solid items/structures (ones with a physics body) and hulls
bool OnCollision(Fixture f1, Fixture f2, Contact contact)
SubmarineBody(Submarine sub, bool showErrorMessages=true)
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,...