Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs
3 using FarseerPhysics;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Xml.Linq;
10 
12 {
13  partial class Sonar : Powered, IServerSerializable, IClientSerializable
14  {
15  public enum BlipType
16  {
17  Default,
18  Disruption,
19  Destructible,
20  Door,
21  LongRange
22  }
23 
24  private PathFinder pathFinder;
25 
26  private readonly bool dynamicDockingIndicator = true;
27 
28  private bool unsentChanges;
29  private float networkUpdateTimer;
30 
31  public GUIButton SonarModeSwitch { get; private set; }
32  private GUITickBox activeTickBox, passiveTickBox;
33  private GUITextBlock signalWarningText;
34 
35  private GUIFrame lowerAreaFrame;
36 
37  private GUIScrollBar zoomSlider;
38 
39  private GUIButton directionalModeSwitch;
40  private Vector2? pingDragDirection = null;
41 
45  private GUIButton mineralScannerSwitch;
46 
47  private GUIFrame controlContainer;
48 
49  private GUICustomComponent sonarView;
50 
51  private Sprite directionalPingBackground;
52  private Sprite[] directionalPingButton;
53 
54  private Sprite pingCircle, directionalPingCircle;
55  private Sprite screenOverlay, screenBackground;
56 
57  private Sprite sonarBlip;
58  private Sprite lineSprite;
59 
60  private readonly Dictionary<Identifier, Tuple<Sprite, Color>> targetIcons = new Dictionary<Identifier, Tuple<Sprite, Color>>();
61 
62  private float displayBorderSize;
63 
64  private List<SonarBlip> sonarBlips;
65 
66  private float prevPassivePingRadius;
67 
68  private Vector2 center;
69 
73  public float DisplayScale
74  {
75  get;
76  private set;
77  } = 1.0f;
78 
79  private const float DisruptionUpdateInterval = 0.2f;
80  private float disruptionUpdateTimer;
81 
82  private const float LongRangeUpdateInterval = 10.0f;
83  private float longRangeUpdateTimer;
84 
85  private float showDirectionalIndicatorTimer;
86 
87  private readonly List<LevelObject> nearbyObjects = new List<LevelObject>();
88  private const float NearbyObjectUpdateInterval = 1.0f;
89  float nearbyObjectUpdateTimer;
90 
91  private readonly List<Submarine> connectedSubs = new List<Submarine>();
92  private const float ConnectedSubUpdateInterval = 1.0f;
93  float connectedSubUpdateTimer;
94 
95  private readonly List<(Vector2 pos, float strength)> disruptedDirections = new List<(Vector2 pos, float strength)>();
96 
97  private readonly Dictionary<object, CachedDistance> markerDistances = new Dictionary<object, CachedDistance>();
98 
99  private readonly Color positiveColor = Color.Green;
100  private readonly Color warningColor = Color.Orange;
101  private readonly Color negativeColor = Color.Red;
102  private readonly Color markerColor = Color.Red;
103 
104  public static readonly Vector2 controlBoxSize = new Vector2(0.33f, 0.32f);
105  public static readonly Vector2 controlBoxOffset = new Vector2(0.025f, 0);
106  private static readonly float sonarAreaSize = 1.09f;
107 
108  private static readonly Dictionary<BlipType, Color[]> blipColorGradient = new Dictionary<BlipType, Color[]>()
109  {
110  {
111  BlipType.Default,
112  new Color[] { Color.TransparentBlack, new Color(0, 50, 160), new Color(0, 133, 166), new Color(2, 159, 30), new Color(255, 255, 255) }
113  },
114  {
115  BlipType.Disruption,
116  new Color[] { Color.TransparentBlack, new Color(254, 68, 19), new Color(255, 220, 62), new Color(255, 255, 255) }
117  },
118  {
119  BlipType.Destructible,
120  new Color[] { Color.TransparentBlack, new Color(94, 114, 73) * 0.8f, new Color(255, 236, 151) * 0.8f, new Color(242, 243, 194) * 0.8f }
121  },
122  {
123  BlipType.Door,
124  new Color[] { Color.TransparentBlack, new Color(73, 78, 86), new Color(66, 94, 100), new Color(47, 115, 58), new Color(255, 255, 255) }
125  },
126  {
127  BlipType.LongRange,
128  new Color[] { Color.TransparentBlack, Color.TransparentBlack, new Color(254, 68, 19) * 0.8f, Color.TransparentBlack }
129  }
130  };
131 
132  private float prevDockingDist;
133 
134  public Vector2 DisplayOffset { get; private set; }
135 
136  public float DisplayRadius { get; private set; }
137 
138  public static Vector2 GUISizeCalculation => Vector2.One * Math.Min(GUI.RelativeHorizontalAspectRatio, 1f) * sonarAreaSize;
139 
140  private List<(Vector2 center, List<Item> resources)> MineralClusters { get; set; }
141 
142  private readonly List<GUITextBlock> textBlocksToScaleAndNormalize = new List<GUITextBlock>();
143 
144  private bool isConnectedToSteering;
145 
146  private static LocalizedString caveLabel;
147 
148 
149  [Serialize(false, IsPropertySaveable.Yes)]
150  public bool RightLayout
151  {
152  get;
153  set;
154  }
155 
156  public override bool RecreateGUIOnResolutionChange => true;
157 
158  partial void InitProjSpecific(ContentXElement element)
159  {
160  System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast<BlipType>().All(t => blipColorGradient.ContainsKey(t)));
161  sonarBlips = new List<SonarBlip>();
162 
163  caveLabel =
164  TextManager.Get("cave").Fallback(
165  TextManager.Get("missiontype.nest"));
166 
167  foreach (var subElement in element.Elements())
168  {
169  switch (subElement.Name.ToString().ToLowerInvariant())
170  {
171  case "pingcircle":
172  pingCircle = new Sprite(subElement);
173  break;
174  case "directionalpingcircle":
175  directionalPingCircle = new Sprite(subElement);
176  break;
177  case "directionalpingbackground":
178  directionalPingBackground = new Sprite(subElement);
179  break;
180  case "directionalpingbutton":
181  if (directionalPingButton == null) { directionalPingButton = new Sprite[3]; }
182  int index = subElement.GetAttributeInt("index", 0);
183  directionalPingButton[index] = new Sprite(subElement);
184  break;
185  case "screenoverlay":
186  screenOverlay = new Sprite(subElement);
187  break;
188  case "screenbackground":
189  screenBackground = new Sprite(subElement);
190  break;
191  case "blip":
192  sonarBlip = new Sprite(subElement);
193  break;
194  case "linesprite":
195  lineSprite = new Sprite(subElement);
196  break;
197  case "icon":
198  var targetIconSprite = new Sprite(subElement);
199  var color = subElement.GetAttributeColor("color", Color.White);
200  targetIcons.Add(subElement.GetAttributeIdentifier("identifier", Identifier.Empty),
201  new Tuple<Sprite, Color>(targetIconSprite, color));
202  break;
203  }
204  }
205  CreateGUI();
206  }
207 
208  protected override void OnResolutionChanged()
209  {
210  UpdateGUIElements();
211  }
212 
213  protected override void CreateGUI()
214  {
215  isConnectedToSteering = item.GetComponent<Steering>() != null;
216  Vector2 size = isConnectedToSteering ? controlBoxSize : new Vector2(0.46f, 0.4f);
217 
218  controlContainer = new GUIFrame(new RectTransform(size, GuiFrame.RectTransform, Anchor.BottomLeft), "ItemUI");
219  if (!isConnectedToSteering && !GUI.IsFourByThree())
220  {
221  controlContainer.RectTransform.MaxSize = new Point((int)(380 * GUI.xScale), (int)(300 * GUI.yScale));
222  }
223  var paddedControlContainer = new GUIFrame(new RectTransform(controlContainer.Rect.Size - GUIStyle.ItemFrameMargin, controlContainer.RectTransform, Anchor.Center)
224  {
225  AbsoluteOffset = GUIStyle.ItemFrameOffset
226  }, style: null);
227  // Based on the height difference to the steering control box so that the elements keep the same size
228  float extraHeight = 0.0694f;
229  var sonarModeArea = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.TopCenter), style: null);
230  SonarModeSwitch = new GUIButton(new RectTransform(new Vector2(0.2f, 1), sonarModeArea.RectTransform), string.Empty, style: "SwitchVertical")
231  {
232  UserData = UIHighlightAction.ElementId.SonarModeSwitch,
233  Selected = false,
234  Enabled = true,
235  ClickSound = GUISoundType.UISwitch,
236  OnClicked = (button, data) =>
237  {
238  button.Selected = !button.Selected;
239  CurrentMode = button.Selected ? Mode.Active : Mode.Passive;
240  if (GameMain.Client != null)
241  {
242  unsentChanges = true;
244  }
245  return true;
246  }
247  };
248  var sonarModeRightSide = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.8f), sonarModeArea.RectTransform, Anchor.CenterLeft)
249  {
250  RelativeOffset = new Vector2(SonarModeSwitch.RectTransform.RelativeSize.X, 0)
251  }, style: null);
252  passiveTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.TopLeft),
253  TextManager.Get("SonarPassive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall")
254  {
255  UserData = UIHighlightAction.ElementId.PassiveSonarIndicator,
256  ToolTip = TextManager.Get("SonarTipPassive"),
257  Selected = true,
258  Enabled = false
259  };
260  activeTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.45f), sonarModeRightSide.RectTransform, Anchor.BottomLeft),
261  TextManager.Get("SonarActive"), font: GUIStyle.SubHeadingFont, style: "IndicatorLightRedSmall")
262  {
263  UserData = UIHighlightAction.ElementId.ActiveSonarIndicator,
264  ToolTip = TextManager.Get("SonarTipActive"),
265  Selected = false,
266  Enabled = false
267  };
268  passiveTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
269  activeTickBox.TextBlock.OverrideTextColor(GUIStyle.TextColorNormal);
270 
271  textBlocksToScaleAndNormalize.Clear();
272  textBlocksToScaleAndNormalize.Add(passiveTickBox.TextBlock);
273  textBlocksToScaleAndNormalize.Add(activeTickBox.TextBlock);
274 
275  lowerAreaFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.4f + extraHeight), paddedControlContainer.RectTransform, Anchor.BottomCenter), style: null);
276  var zoomContainer = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.TopCenter), style: null);
277  var zoomText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.6f), zoomContainer.RectTransform, Anchor.CenterLeft),
278  TextManager.Get("SonarZoom"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight);
279  textBlocksToScaleAndNormalize.Add(zoomText);
280  zoomSlider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 0.8f), zoomContainer.RectTransform, Anchor.CenterLeft)
281  {
282  RelativeOffset = new Vector2(0.35f, 0)
283  }, barSize: 0.15f, isHorizontal: true, style: "DeviceSlider")
284  {
285  OnMoved = (scrollbar, scroll) =>
286  {
287  zoom = MathHelper.Lerp(MinZoom, MaxZoom, scroll);
288  if (GameMain.Client != null)
289  {
290  unsentChanges = true;
292  }
293  return true;
294  }
295  };
296 
297  new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedControlContainer.RectTransform, Anchor.Center), style: "HorizontalLine")
298  {
299  UserData = "horizontalline"
300  };
301 
302  var directionalModeFrame = new GUIFrame(new RectTransform(new Vector2(1, 0.45f), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null)
303  {
304  UserData = UIHighlightAction.ElementId.DirectionalSonarFrame
305  };
306  directionalModeSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), directionalModeFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal")
307  {
308  OnClicked = (button, data) =>
309  {
310  useDirectionalPing = !useDirectionalPing;
311  button.Selected = useDirectionalPing;
312  if (GameMain.Client != null)
313  {
314  unsentChanges = true;
316  }
317  return true;
318  }
319  };
320  var directionalModeSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), directionalModeFrame.RectTransform, Anchor.CenterRight),
321  TextManager.Get("SonarDirectionalPing"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft);
322  textBlocksToScaleAndNormalize.Add(directionalModeSwitchText);
323 
324  if (HasMineralScanner)
325  {
326  AddMineralScannerSwitchToGUI();
327  }
328  else
329  {
330  mineralScannerSwitch = null;
331  }
332 
333  GuiFrame.CanBeFocused = false;
334 
335  GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize);
336 
337  sonarView = new GUICustomComponent(new RectTransform(Vector2.One * 0.7f, GuiFrame.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight),
338  (spriteBatch, guiCustomComponent) => { DrawSonar(spriteBatch, guiCustomComponent.Rect); }, null);
339 
340  signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), sonarView.RectTransform, Anchor.Center, Pivot.BottomCenter),
341  "", warningColor, GUIStyle.LargeFont, Alignment.Center);
342 
343  // Setup layout for nav terminal
344  if (isConnectedToSteering || RightLayout)
345  {
346  controlContainer.RectTransform.AbsoluteOffset = Point.Zero;
347  controlContainer.RectTransform.RelativeOffset = controlBoxOffset;
348  controlContainer.RectTransform.SetPosition(Anchor.TopRight);
349  sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest;
350  if (HasMineralScanner) { PreventMineralScannerOverlap(); }
351  sonarView.RectTransform.SetPosition(Anchor.CenterLeft);
353  GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize);
354  }
355  else if (GUI.RelativeHorizontalAspectRatio > 0.75f)
356  {
357  sonarView.RectTransform.RelativeOffset = new Vector2(0.13f * GUI.RelativeHorizontalAspectRatio, 0);
358  sonarView.RectTransform.SetPosition(Anchor.BottomRight);
359  }
360  var handle = GuiFrame.GetChild<GUIDragHandle>();
361  if (handle != null)
362  {
363  handle.RectTransform.Parent = controlContainer.RectTransform;
364  handle.RectTransform.Resize(Vector2.One);
365  handle.RectTransform.SetAsFirstChild();
366  }
367  }
368 
369  private void SetPingDirection(Vector2 direction)
370  {
371  pingDirection = direction;
372  if (GameMain.Client != null)
373  {
374  unsentChanges = true;
376  }
377  }
378 
379  private Vector2 GetTransducerPos()
380  {
381  if (!UseTransducers || connectedTransducers.Count == 0)
382  {
383  //use the position of the sub if the item is static (no body) and inside a sub
384  return item.Submarine != null && item.body == null ? item.Submarine.WorldPosition : item.WorldPosition;
385  }
386 
387  Vector2 transducerPosSum = Vector2.Zero;
388  foreach (ConnectedTransducer transducer in connectedTransducers)
389  {
390  if (transducer.Transducer.Item.Submarine != null && !CenterOnTransducers)
391  {
392  return transducer.Transducer.Item.Submarine.WorldPosition;
393  }
394  transducerPosSum += transducer.Transducer.Item.WorldPosition;
395  }
396  return transducerPosSum / connectedTransducers.Count;
397  }
398 
399 
400  public override void OnItemLoaded()
401  {
402  base.OnItemLoaded();
403  zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom);
404  if (HasMineralScanner && mineralScannerSwitch == null)
405  {
406  AddMineralScannerSwitchToGUI();
407  GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize);
408  }
409  //make the sonarView customcomponent render the steering view so it gets drawn in front of the sonar
410  item.GetComponent<Steering>()?.AttachToSonarHUD(sonarView);
411  }
412 
413  private void AddMineralScannerSwitchToGUI()
414  {
415  // First adjust other elements to make room for the additional switch
416  controlContainer.RectTransform.RelativeSize = new Vector2(
417  controlContainer.RectTransform.RelativeSize.X,
418  controlContainer.RectTransform.RelativeSize.Y * 1.25f);
422  lowerAreaFrame.Parent.GetChildByUserData("horizontalline").RectTransform.RelativeOffset =
423  new Vector2(0.0f, -0.1f);
424  lowerAreaFrame.RectTransform.RelativeSize = new Vector2(
425  lowerAreaFrame.RectTransform.RelativeSize.X,
426  lowerAreaFrame.RectTransform.RelativeSize.Y * 1.2f);
427  zoomSlider.Parent.RectTransform.RelativeSize = new Vector2(
428  zoomSlider.Parent.RectTransform.RelativeSize.X,
429  zoomSlider.Parent.RectTransform.RelativeSize.Y * (2.0f / 3.0f));
430  directionalModeSwitch.Parent.RectTransform.RelativeSize = new Vector2(
431  directionalModeSwitch.Parent.RectTransform.RelativeSize.X,
432  zoomSlider.Parent.RectTransform.RelativeSize.Y);
433  directionalModeSwitch.Parent.RectTransform.SetPosition(Anchor.Center);
434 
435  // Then add the scanner switch
436  var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, zoomSlider.Parent.RectTransform.RelativeSize.Y), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null);
437  mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal")
438  {
440  OnClicked = (button, data) =>
441  {
443  button.Selected = UseMineralScanner;
444  if (GameMain.Client != null)
445  {
446  unsentChanges = true;
448  }
449 
450  return true;
451  }
452  };
453  var mineralScannerSwitchText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1), mineralScannerFrame.RectTransform, Anchor.CenterRight),
454  TextManager.Get("SonarMineralScanner"), GUIStyle.TextColorNormal, GUIStyle.SubHeadingFont, Alignment.CenterLeft);
455  textBlocksToScaleAndNormalize.Add(mineralScannerSwitchText);
456 
457  PreventMineralScannerOverlap();
458  }
459 
460  private void PreventMineralScannerOverlap()
461  {
462  if (item.GetComponent<Steering>() is { } steering && controlContainer is { } container)
463  {
464  int containerBottom = container.Rect.Y + container.Rect.Height,
465  steeringTop = steering.ControlContainer.Rect.Top;
466 
467  int amountRaised = 0;
468 
469  while (GetContainerBottom() > steeringTop) { amountRaised++; }
470 
471  container.RectTransform.AbsoluteOffset = new Point(0, -amountRaised);
472 
473  int GetContainerBottom() => containerBottom - amountRaised;
474  }
475  }
476 
477  public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
478  {
479  showDirectionalIndicatorTimer -= deltaTime;
480  if (GameMain.Client != null)
481  {
482  if (unsentChanges)
483  {
484  if (networkUpdateTimer <= 0.0f)
485  {
486  item.CreateClientEvent(this);
488  networkUpdateTimer = 0.1f;
489  unsentChanges = false;
490  }
491  }
492  networkUpdateTimer -= deltaTime;
493  }
494 
495  connectedSubUpdateTimer -= deltaTime;
496  if (connectedSubUpdateTimer <= 0.0f)
497  {
498  connectedSubs.Clear();
499  if (UseTransducers)
500  {
501  foreach (var transducer in connectedTransducers)
502  {
503  if (transducer.Transducer.Item.Submarine == null) { continue; }
504  if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { continue; }
505  connectedSubs.AddRange(transducer.Transducer.Item.Submarine.GetConnectedSubs());
506  }
507  }
508  else if (item.Submarine != null)
509  {
510  connectedSubs.AddRange(item.Submarine?.GetConnectedSubs());
511  }
512  connectedSubUpdateTimer = ConnectedSubUpdateInterval;
513  }
514 
515  Steering steering = item.GetComponent<Steering>();
516  if (sonarView.Rect.Contains(PlayerInput.MousePosition) &&
517  (GUI.MouseOn == null || GUI.MouseOn == sonarView || sonarView.IsParentOf(GUI.MouseOn) || GUI.MouseOn == steering?.GuiFrame || (steering?.GuiFrame?.IsParentOf(GUI.MouseOn) ?? false)))
518  {
519  float scrollSpeed = PlayerInput.ScrollWheelSpeed / 1000.0f;
520  if (Math.Abs(scrollSpeed) > 0.0001f)
521  {
522  zoomSlider.BarScroll += PlayerInput.ScrollWheelSpeed / 1000.0f;
523  zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll);
524  }
525  }
526 
527  Vector2 transducerCenter = GetTransducerPos();
528 
529  if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null)
530  {
531  Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f;
532  DisplayOffset = Vector2.Lerp(DisplayOffset, worldFocusPos - transducerCenter, 0.1f);
533  }
534  else
535  {
536  DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f);
537  }
538  transducerCenter += DisplayOffset;
539 
540  float distort = MathHelper.Clamp(1.0f - item.Condition / item.MaxCondition, 0.0f, 1.0f);
541  for (int i = sonarBlips.Count - 1; i >= 0; i--)
542  {
543  sonarBlips[i].FadeTimer -= deltaTime * MathHelper.Lerp(0.5f, 2.0f, distort);
544  sonarBlips[i].Position += sonarBlips[i].Velocity * deltaTime;
545 
546  if (sonarBlips[i].FadeTimer <= 0.0f) { sonarBlips.RemoveAt(i); }
547  }
548 
549  //sonar view can only get focus when the cursor is inside the circle
550  sonarView.CanBeFocused =
551  Vector2.DistanceSquared(sonarView.Rect.Center.ToVector2(), PlayerInput.MousePosition) <
552  (sonarView.Rect.Width / 2 * sonarView.Rect.Width / 2);
553 
555  {
556  if (MineralClusters == null)
557  {
558  MineralClusters = new List<(Vector2, List<Item>)>();
559  Level.Loaded.PathPoints.ForEach(p => p.ClusterLocations.ForEach(c => AddIfValid(c)));
560  Level.Loaded.AbyssResources.ForEach(c => AddIfValid(c));
561 
562  void AddIfValid(Level.ClusterLocation c)
563  {
564  if (c.Resources == null) { return; }
565  if (c.Resources.None(i => i != null && !i.Removed && i.Tags.Contains("ore"))) { return; }
566  var pos = Vector2.Zero;
567  foreach (var r in c.Resources)
568  {
569  pos += r.WorldPosition;
570  }
571  pos /= c.Resources.Count;
572  MineralClusters.Add((center: pos, resources: c.Resources));
573  }
574  }
575  else
576  {
577  MineralClusters.RemoveAll(c => c.resources == null || c.resources.None() || c.resources.All(i => i == null || i.Removed));
578  }
579  }
580 
581  if (UseTransducers && connectedTransducers.Count == 0)
582  {
583  return;
584  }
585 
586  if (Level.Loaded != null)
587  {
588  nearbyObjectUpdateTimer -= deltaTime;
589  if (nearbyObjectUpdateTimer <= 0.0f)
590  {
591  nearbyObjects.Clear();
592  foreach (var nearbyObject in Level.Loaded.LevelObjectManager.GetAllObjects(transducerCenter, range * zoom))
593  {
594  if (!nearbyObject.VisibleOnSonar) { continue; }
595  float objectRange = range + nearbyObject.SonarRadius;
596  if (Vector2.DistanceSquared(transducerCenter, nearbyObject.WorldPosition) < objectRange * objectRange)
597  {
598  nearbyObjects.Add(nearbyObject);
599  }
600  }
601  nearbyObjectUpdateTimer = NearbyObjectUpdateInterval;
602  }
603 
604  List<LevelTrigger> ballastFloraSpores = new List<LevelTrigger>();
605  Dictionary<LevelTrigger, Vector2> levelTriggerFlows = new Dictionary<LevelTrigger, Vector2>();
606  for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
607  {
608  var activePing = activePings[pingIndex];
609  float pingRange = range * activePing.State / zoom;
610  foreach (LevelObject levelObject in nearbyObjects)
611  {
612  if (levelObject.Triggers == null) { continue; }
613  //gather all nearby triggers that are causing the water to flow into the dictionary
614  foreach (LevelTrigger trigger in levelObject.Triggers)
615  {
616  Vector2 flow = trigger.GetWaterFlowVelocity();
617  //ignore ones that are barely doing anything (flow^2 <= 1)
618  if (flow.LengthSquared() >= 1.0f && !levelTriggerFlows.ContainsKey(trigger))
619  {
620  levelTriggerFlows.Add(trigger, flow);
621  }
622  if (!trigger.InfectIdentifier.IsEmpty &&
623  Vector2.DistanceSquared(transducerCenter, trigger.WorldPosition) < pingRange / 2 * pingRange / 2)
624  {
625  ballastFloraSpores.Add(trigger);
626  }
627  }
628  }
629  }
630 
631  foreach (KeyValuePair<LevelTrigger, Vector2> triggerFlow in levelTriggerFlows)
632  {
633  LevelTrigger trigger = triggerFlow.Key;
634  Vector2 flow = triggerFlow.Value;
635 
636  float flowMagnitude = flow.Length();
637  if (Rand.Range(0.0f, 1.0f) < flowMagnitude / 1000.0f)
638  {
639  float edgeDist = Rand.Range(0.0f, 1.0f);
640  Vector2 blipPos = trigger.WorldPosition + Rand.Vector(trigger.ColliderRadius * edgeDist);
641  Vector2 blipVel = flow;
642 
643  //go through other triggers in range and add the flows of the ones that the blip is inside
644  foreach (KeyValuePair<LevelTrigger, Vector2> triggerFlow2 in levelTriggerFlows)
645  {
646  LevelTrigger trigger2 = triggerFlow2.Key;
647  if (trigger2 != trigger && Vector2.DistanceSquared(blipPos, trigger2.WorldPosition) < trigger2.ColliderRadius * trigger2.ColliderRadius)
648  {
649  Vector2 trigger2flow = triggerFlow2.Value;
650  if (trigger2.ForceFalloff) trigger2flow *= 1.0f - Vector2.Distance(blipPos, trigger2.WorldPosition) / trigger2.ColliderRadius;
651  blipVel += trigger2flow;
652  }
653  }
654  var flowBlip = new SonarBlip(blipPos, Rand.Range(0.5f, 1.0f), 1.0f)
655  {
656  Velocity = blipVel * Rand.Range(1.0f, 5.0f),
657  Size = new Vector2(MathHelper.Lerp(0.4f, 5f, flowMagnitude / 500.0f), 0.2f),
658  Rotation = (float)Math.Atan2(-blipVel.Y, blipVel.X)
659  };
660  sonarBlips.Add(flowBlip);
661  }
662  }
663 
664  foreach (LevelTrigger spore in ballastFloraSpores)
665  {
666  Vector2 blipPos = spore.WorldPosition + Rand.Vector(spore.ColliderRadius * Rand.Range(0.0f, 1.0f));
667  SonarBlip sporeBlip = new SonarBlip(blipPos, Rand.Range(0.1f, 0.5f), 0.5f)
668  {
669  Rotation = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi),
670  BlipType = BlipType.Default,
671  Velocity = Rand.Vector(100f, Rand.RandSync.Unsynced)
672  };
673 
674  sonarBlips.Add(sporeBlip);
675  }
676 
677  float outsideLevelFlow = 0.0f;
678  if (transducerCenter.X < 0.0f)
679  {
680  outsideLevelFlow = Math.Abs(transducerCenter.X * 0.001f);
681  }
682  else if (transducerCenter.X > Level.Loaded.Size.X)
683  {
684  outsideLevelFlow = -(transducerCenter.X - Level.Loaded.Size.X) * 0.001f;
685  }
686 
687  if (Rand.Range(0.0f, 100.0f) < Math.Abs(outsideLevelFlow))
688  {
689  Vector2 blipPos = transducerCenter + Rand.Vector(Rand.Range(0.0f, range));
690  var flowBlip = new SonarBlip(blipPos, Rand.Range(0.5f, 1.0f), 1.0f)
691  {
692  Velocity = Vector2.UnitX * outsideLevelFlow * Rand.Range(50.0f, 100.0f),
693  Size = new Vector2(Rand.Range(0.4f, 5f), 0.2f),
694  Rotation = 0.0f
695  };
696  sonarBlips.Add(flowBlip);
697  }
698  }
699 
700  if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null)
701  {
702  float dockingDist = Vector2.Distance(steering.ActiveDockingSource.Item.WorldPosition, steering.DockingTarget.Item.WorldPosition);
703  if (prevDockingDist > steering.DockingAssistThreshold && dockingDist <= steering.DockingAssistThreshold)
704  {
705  zoomSlider.BarScroll = 0.25f;
706  zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll));
707  }
708  else if (prevDockingDist > steering.DockingAssistThreshold * 0.75f && dockingDist <= steering.DockingAssistThreshold * 0.75f)
709  {
710  zoomSlider.BarScroll = 0.5f;
711  zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll));
712  }
713  else if (prevDockingDist > steering.DockingAssistThreshold * 0.5f && dockingDist <= steering.DockingAssistThreshold * 0.5f)
714  {
715  zoomSlider.BarScroll = 0.25f;
716  zoom = Math.Max(zoom, MathHelper.Lerp(MinZoom, MaxZoom, zoomSlider.BarScroll));
717  }
718  prevDockingDist = Math.Min(dockingDist, prevDockingDist);
719  }
720  else
721  {
722  prevDockingDist = float.MaxValue;
723  }
724 
725  if (steering != null && directionalPingButton != null)
726  {
727  steering.SteerRadius = useDirectionalPing && pingDragDirection != null ?
728  -1.0f :
730  (float?)((sonarView.Rect.Width / 2) - (directionalPingButton[0].size.X * sonarView.Rect.Width / screenBackground.size.X)) :
731  null;
732  }
733 
734  if (useDirectionalPing)
735  {
736  Vector2 newDragDir = Vector2.Normalize(PlayerInput.MousePosition - sonarView.Rect.Center.ToVector2());
737  if (MouseInDirectionalPingRing(sonarView.Rect, true) && PlayerInput.PrimaryMouseButtonDown())
738  {
739  pingDragDirection = newDragDir;
740  }
741 
742  if (pingDragDirection != null && PlayerInput.PrimaryMouseButtonHeld())
743  {
744  float newAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(newDragDir));
745  SetPingDirection(new Vector2((float)Math.Cos(newAngle), (float)Math.Sin(newAngle)));
746  }
747  else
748  {
749  pingDragDirection = null;
750  }
751  }
752  else
753  {
754  pingDragDirection = null;
755  }
756 
757  disruptionUpdateTimer -= deltaTime;
758  for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
759  {
760  var activePing = activePings[pingIndex];
761  float pingRadius = DisplayRadius * activePing.State / zoom;
762  if (disruptionUpdateTimer <= 0.0f) { UpdateDisruptions(transducerCenter, pingRadius / DisplayScale); }
763  Ping(transducerCenter, transducerCenter,
764  pingRadius, activePing.PrevPingRadius, DisplayScale, range / zoom, passive: false, pingStrength: 2.0f);
765  activePing.PrevPingRadius = pingRadius;
766  }
767  if (disruptionUpdateTimer <= 0.0f)
768  {
769  disruptionUpdateTimer = DisruptionUpdateInterval;
770  }
771 
772  longRangeUpdateTimer -= deltaTime;
773  if (longRangeUpdateTimer <= 0.0f)
774  {
775  foreach (Character c in Character.CharacterList)
776  {
777  if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; }
778  if (c.Params.HideInSonar) { continue; }
779 
780  if (!c.IsUnconscious && c.Params.DistantSonarRange > 0.0f &&
781  ((c.WorldPosition - transducerCenter) * DisplayScale).LengthSquared() > DisplayRadius * DisplayRadius)
782  {
783  Vector2 targetVector = c.WorldPosition - transducerCenter;
784  if (targetVector.LengthSquared() > MathUtils.Pow2(c.Params.DistantSonarRange)) { continue; }
785  float dist = targetVector.Length();
786  Vector2 targetDir = targetVector / dist;
787  int blipCount = (int)MathHelper.Clamp(c.Mass, 50, 200);
788  for (int i = 0; i < blipCount; i++)
789  {
790  float angle = Rand.Range(-0.5f, 0.5f);
791  Vector2 blipDir = MathUtils.RotatePoint(targetDir, angle);
792  Vector2 invBlipDir = MathUtils.RotatePoint(targetDir, -angle);
793  var longRangeBlip = new SonarBlip(transducerCenter + blipDir * Range * 0.9f, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange)
794  {
795  Velocity = -invBlipDir * (MathUtils.Round(Rand.Range(8000.0f, 15000.0f), 2000.0f) - Math.Abs(angle * angle * 10000.0f)),
796  Rotation = (float)Math.Atan2(-invBlipDir.Y, invBlipDir.X),
797  Alpha = MathUtils.Pow2((c.Params.DistantSonarRange - dist) / c.Params.DistantSonarRange)
798  };
799  longRangeBlip.Size.Y *= 5.0f;
800  sonarBlips.Add(longRangeBlip);
801  }
802  }
803  }
804  longRangeUpdateTimer = LongRangeUpdateInterval;
805  }
806 
807  if (currentMode == Mode.Active && currentPingIndex != -1)
808  {
809  return;
810  }
811 
812  float passivePingRadius = (float)(Timing.TotalTime % 1.0f);
813  if (passivePingRadius > 0.0f)
814  {
815  if (activePingsCount == 0) { disruptedDirections.Clear(); }
816  //emit "pings" from nearby sound-emitting AITargets to reveal what's around them
817  foreach (AITarget t in AITarget.List)
818  {
819  if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; }
820  if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; }
821 
822  float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter);
823  if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
824 
825  float dist = (float)Math.Sqrt(distSqr);
826  if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500)
827  {
828  Ping(t.WorldPosition, transducerCenter,
829  t.SoundRange * DisplayScale, 0, DisplayScale, range,
830  passive: true, pingStrength: 0.5f, needsToBeInSector: t);
831  if (t.IsWithinSector(transducerCenter))
832  {
833  sonarBlips.Add(new SonarBlip(t.WorldPosition, fadeTimer: 1.0f, scale: MathHelper.Clamp(t.SoundRange / 2000, 1.0f, 5.0f)));
834  }
835  }
836  }
837  }
838  prevPassivePingRadius = passivePingRadius;
839  }
840 
841  private bool MouseInDirectionalPingRing(Rectangle rect, bool onButton)
842  {
843  if (!useDirectionalPing || directionalPingButton == null) { return false; }
844 
845  float endRadius = rect.Width / 2.0f;
846  float startRadius = endRadius - directionalPingButton[0].size.X * rect.Width / screenBackground.size.X;
847 
848  Vector2 center = rect.Center.ToVector2();
849 
850  float dist = Vector2.DistanceSquared(PlayerInput.MousePosition,center);
851 
852  bool retVal = (dist >= startRadius*startRadius) && (dist < endRadius*endRadius);
853  if (onButton)
854  {
855  float pingAngle = MathUtils.VectorToAngle(pingDirection);
856  float mouseAngle = MathUtils.VectorToAngle(Vector2.Normalize(PlayerInput.MousePosition - center));
857  retVal &= Math.Abs(MathUtils.GetShortestAngle(mouseAngle, pingAngle)) < MathHelper.ToRadians(DirectionalPingSector * 0.5f);
858  }
859 
860  return retVal;
861  }
862 
863  private void DrawSonar(SpriteBatch spriteBatch, Rectangle rect)
864  {
865  displayBorderSize = 0.2f;
866  center = rect.Center.ToVector2();
867  DisplayRadius = (rect.Width / 2.0f) * (1.0f - displayBorderSize);
868  DisplayScale = DisplayRadius / range * zoom;
869 
870  screenBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / screenBackground.size.X);
871 
872  if (useDirectionalPing)
873  {
874  directionalPingBackground?.Draw(spriteBatch, center, 0.0f, rect.Width / directionalPingBackground.size.X);
875  if (directionalPingButton != null)
876  {
877  int buttonSprIndex = 0;
878  if (pingDragDirection != null)
879  {
880  buttonSprIndex = 2;
881  }
882  else if (MouseInDirectionalPingRing(rect, true))
883  {
884  buttonSprIndex = 1;
885  }
886  directionalPingButton[buttonSprIndex]?.Draw(spriteBatch, center, MathUtils.VectorToAngle(pingDirection), rect.Width / directionalPingBackground.size.X);
887  }
888  }
889 
890  if (currentPingIndex != -1)
891  {
892  var activePing = activePings[currentPingIndex];
893  if (activePing.IsDirectional && directionalPingCircle != null)
894  {
895  directionalPingCircle.Draw(spriteBatch, center, Color.White * (1.0f - activePing.State),
896  rotate: MathUtils.VectorToAngle(activePing.Direction),
897  scale: DisplayRadius / directionalPingCircle.size.X * activePing.State);
898  }
899  else
900  {
901  pingCircle.Draw(spriteBatch, center, Color.White * (1.0f - activePing.State), 0.0f, (DisplayRadius * 2 / pingCircle.size.X) * activePing.State);
902  }
903  }
904 
905  float signalStrength = 1.0f;
906  if (UseTransducers)
907  {
908  signalStrength = 0.0f;
909  foreach (ConnectedTransducer connectedTransducer in connectedTransducers)
910  {
911  signalStrength = Math.Max(signalStrength, connectedTransducer.SignalStrength);
912  }
913  }
914 
915  Vector2 transducerCenter = GetTransducerPos();// + DisplayOffset;
916 
917  if (sonarBlips.Count > 0)
918  {
919  float blipScale = 0.08f * (float)Math.Sqrt(zoom) * (rect.Width / 700.0f);
920  spriteBatch.End();
921  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive);
922 
923  foreach (SonarBlip sonarBlip in sonarBlips)
924  {
925  DrawBlip(spriteBatch, sonarBlip, transducerCenter + DisplayOffset, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale);
926  }
927 
928  spriteBatch.End();
929  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
930  }
931 
932  if (item.Submarine != null && !DetectSubmarineWalls)
933  {
934  transducerCenter += DisplayOffset;
935  DrawDockingPorts(spriteBatch, transducerCenter, signalStrength);
936  DrawOwnSubmarineBorders(spriteBatch, transducerCenter, signalStrength);
937  }
938  else
939  {
940  DisplayOffset = Vector2.Zero;
941  }
942 
943  float directionalPingVisibility = useDirectionalPing && currentMode == Mode.Active ? 1.0f : showDirectionalIndicatorTimer;
944  if (directionalPingVisibility > 0.0f)
945  {
946  Vector2 sector1 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(DirectionalPingSector * 0.5f));
947  Vector2 sector2 = MathUtils.RotatePointAroundTarget(pingDirection * DisplayRadius, Vector2.Zero, MathHelper.ToRadians(-DirectionalPingSector * 0.5f));
948  DrawLine(spriteBatch, Vector2.Zero, sector1, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3);
949  DrawLine(spriteBatch, Vector2.Zero, sector2, Color.LightCyan * 0.2f * directionalPingVisibility, width: 3);
950  }
951 
952  if (GameMain.DebugDraw)
953  {
954  GUI.DrawString(spriteBatch, rect.Location.ToVector2(), sonarBlips.Count.ToString(), Color.White);
955  }
956 
957  screenOverlay?.Draw(spriteBatch, center, 0.0f, rect.Width / screenOverlay.size.X);
958 
959  if (signalStrength <= 0.5f)
960  {
961  signalWarningText.Text = TextManager.Get(signalStrength <= 0.0f ? "SonarNoSignal" : "SonarSignalWeak");
962  signalWarningText.Color = signalStrength <= 0.0f ? negativeColor : warningColor;
963  signalWarningText.Visible = true;
964  return;
965  }
966  else
967  {
968  signalWarningText.Visible = false;
969  }
970 
971  foreach (AITarget aiTarget in AITarget.List)
972  {
973  if (aiTarget.InDetectable) { continue; }
974  if (aiTarget.SonarLabel.IsNullOrEmpty() || aiTarget.SoundRange <= 0.0f) { continue; }
975 
976  if (Vector2.DistanceSquared(aiTarget.WorldPosition, transducerCenter) < aiTarget.SoundRange * aiTarget.SoundRange)
977  {
978  DrawMarker(spriteBatch,
979  aiTarget.SonarLabel.Value,
980  aiTarget.SonarIconIdentifier,
981  aiTarget,
982  aiTarget.WorldPosition, transducerCenter,
983  DisplayScale, center, DisplayRadius * 0.975f);
984  }
985  }
986 
987  if (GameMain.GameSession == null) { return; }
988 
989  if (Level.Loaded != null)
990  {
991  if (Level.Loaded.StartLocation?.Type is { ShowSonarMarker: true })
992  {
993  DrawMarker(spriteBatch,
994  Level.Loaded.StartLocation.DisplayName.Value,
995  (Level.Loaded.StartOutpost != null ? "outpost" : "location").ToIdentifier(),
996  "startlocation",
997  Level.Loaded.StartExitPosition, transducerCenter,
998  DisplayScale, center, DisplayRadius);
999  }
1000 
1001  if (Level.Loaded is { EndLocation.Type.ShowSonarMarker: true, Type: LevelData.LevelType.LocationConnection })
1002  {
1003  DrawMarker(spriteBatch,
1004  Level.Loaded.EndLocation.DisplayName.Value,
1005  (Level.Loaded.EndOutpost != null ? "outpost" : "location").ToIdentifier(),
1006  "endlocation",
1007  Level.Loaded.EndExitPosition, transducerCenter,
1008  DisplayScale, center, DisplayRadius);
1009  }
1010 
1011  for (int i = 0; i < Level.Loaded.Caves.Count; i++)
1012  {
1013  var cave = Level.Loaded.Caves[i];
1014  if (cave.MissionsToDisplayOnSonar.None()) { continue; }
1015  DrawMarker(spriteBatch,
1016  caveLabel.Value,
1017  "cave".ToIdentifier(),
1018  "cave" + i,
1019  cave.StartPos.ToVector2(), transducerCenter,
1020  DisplayScale, center, DisplayRadius);
1021  }
1022  }
1023 
1024  int missionIndex = 0;
1025  foreach (Mission mission in GameMain.GameSession.Missions)
1026  {
1027  int i = 0;
1028  foreach ((LocalizedString label, Vector2 position) in mission.SonarLabels)
1029  {
1030  if (!string.IsNullOrEmpty(label.Value))
1031  {
1032  DrawMarker(spriteBatch,
1033  label.Value,
1034  mission.SonarIconIdentifier,
1035  "mission" + missionIndex + ":" + i,
1036  position, transducerCenter,
1037  DisplayScale, center, DisplayRadius * 0.95f);
1038  }
1039  i++;
1040  }
1041  missionIndex++;
1042  }
1043 
1044  if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
1045  (item.CurrentHull == null || !DetectSubmarineWalls))
1046  {
1047  foreach (var c in MineralClusters)
1048  {
1049  var unobtainedMinerals = c.resources.Where(i => i != null && i.GetComponent<Holdable>() is { Attached: true });
1050  if (unobtainedMinerals.None()) { continue; }
1051  if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; }
1052  var i = unobtainedMinerals.FirstOrDefault();
1053  if (i == null) { continue; }
1054 
1055  bool disrupted = false;
1056  foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections)
1057  {
1058  float dot = Vector2.Dot(Vector2.Normalize(c.center - transducerCenter), disruptPos);
1059  if (dot > 1.0f - disruptStrength)
1060  {
1061  disrupted = true;
1062  break;
1063  }
1064  }
1065  if (disrupted) { continue; }
1066 
1067  DrawMarker(spriteBatch,
1068  i.Name, "mineral".ToIdentifier(), "mineralcluster" + i,
1069  c.center, transducerCenter,
1070  DisplayScale, center, DisplayRadius * 0.95f,
1071  onlyShowTextOnMouseOver: true);
1072  }
1073  }
1074 
1075  foreach (Submarine sub in Submarine.Loaded)
1076  {
1077  if (!sub.ShowSonarMarker) { continue; }
1078  if (connectedSubs.Contains(sub)) { continue; }
1079  if (Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
1080 
1081  if (item.Submarine != null || Character.Controlled != null)
1082  {
1083  //hide enemy team
1084  if (sub.TeamID == CharacterTeamType.Team1 && (item.Submarine?.TeamID == CharacterTeamType.Team2 || Character.Controlled?.TeamID == CharacterTeamType.Team2))
1085  {
1086  continue;
1087  }
1088  else if (sub.TeamID == CharacterTeamType.Team2 && (item.Submarine?.TeamID == CharacterTeamType.Team1 || Character.Controlled?.TeamID == CharacterTeamType.Team1))
1089  {
1090  continue;
1091  }
1092  }
1093 
1094  DrawMarker(spriteBatch,
1095  sub.Info.DisplayName.Value,
1096  (sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine").ToIdentifier(),
1097  sub,
1098  sub.WorldPosition, transducerCenter,
1099  DisplayScale, center, DisplayRadius * 0.95f);
1100  }
1101 
1102  if (GameMain.DebugDraw)
1103  {
1104  var steering = item.GetComponent<Steering>();
1105  steering?.DebugDrawHUD(spriteBatch, transducerCenter, DisplayScale, DisplayRadius, center);
1106  }
1107  }
1108 
1109  private void DrawOwnSubmarineBorders(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength)
1110  {
1111  float simScale = DisplayScale * Physics.DisplayToSimRation;
1112 
1113  foreach (Submarine submarine in Submarine.Loaded)
1114  {
1115  if (!connectedSubs.Contains(submarine)) { continue; }
1116  if (submarine.HullVertices == null) { continue; }
1117 
1118  Vector2 offset = ConvertUnits.ToSimUnits(submarine.WorldPosition - transducerCenter);
1119 
1120  for (int i = 0; i < submarine.HullVertices.Count; i++)
1121  {
1122  Vector2 start = (submarine.HullVertices[i] + offset) * simScale;
1123  start.Y = -start.Y;
1124  Vector2 end = (submarine.HullVertices[(i + 1) % submarine.HullVertices.Count] + offset) * simScale;
1125  end.Y = -end.Y;
1126 
1127  DrawLine(spriteBatch, start, end, Color.LightBlue * signalStrength * 0.5f, width: 4);
1128  }
1129  }
1130  }
1131 
1132  private void DrawLine(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, int width)
1133  {
1134  bool startOutside = start.LengthSquared() > DisplayRadius * DisplayRadius;
1135  bool endOutside = end.LengthSquared() > DisplayRadius * DisplayRadius;
1136  if (startOutside && endOutside)
1137  {
1138  return;
1139  }
1140  else if (startOutside)
1141  {
1142  if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, end, start, true, out Vector2? intersection1, out _) == 1)
1143  {
1144  DrawLineSprite(spriteBatch, center + intersection1.Value, center + end, color, width: width);
1145  }
1146  }
1147  else if (endOutside)
1148  {
1149  if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, start, end, true, out Vector2? intersection1, out _) == 1)
1150  {
1151  DrawLineSprite(spriteBatch, center + start, center + intersection1.Value, color, width: width);
1152  }
1153  }
1154  else
1155  {
1156  DrawLineSprite(spriteBatch, center + start, center + end, color, width: width);
1157  }
1158  }
1159 
1160  private void DrawLineSprite(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, int width)
1161  {
1162  if (lineSprite == null)
1163  {
1164  GUI.DrawLine(spriteBatch, start, end, color, width: width);
1165  }
1166  else
1167  {
1168  Vector2 dir = end - start;
1169  float angle = (float)Math.Atan2(dir.Y, dir.X);
1170  lineSprite.Draw(spriteBatch, start, color, origin: lineSprite.Origin, rotate: angle,
1171  scale: new Vector2(dir.Length() / lineSprite.size.X, 1.0f));
1172  }
1173  }
1174 
1175 
1176  private void DrawDockingPorts(SpriteBatch spriteBatch, Vector2 transducerCenter, float signalStrength)
1177  {
1178  float scale = DisplayScale;
1179 
1180  Steering steering = item.GetComponent<Steering>();
1181  if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null)
1182  {
1183  DrawDockingIndicator(spriteBatch, steering, ref transducerCenter);
1184  }
1185 
1186  foreach (DockingPort dockingPort in DockingPort.List)
1187  {
1188  if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; }
1189  if (dockingPort.Item.IsHidden) { continue; }
1190  if (dockingPort.Item.Submarine == null) { continue; }
1191  if (dockingPort.Item.Submarine.Info.IsWreck) { continue; }
1192  // docking ports should be shown even if defined as not, if the submarine is the same as the sonar's
1193  if (!dockingPort.Item.Submarine.ShowSonarMarker && dockingPort.Item.Submarine != item.Submarine &&
1194  !dockingPort.Item.Submarine.Info.IsOutpost && !dockingPort.Item.Submarine.Info.IsBeacon)
1195  {
1196  continue;
1197  }
1198 
1199  //don't show the docking ports of the opposing team on the sonar
1200  if (item.Submarine != null &&
1201  item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
1202  dockingPort.Item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle &&
1203  !dockingPort.Item.Submarine.Info.IsOutpost &&
1204  !dockingPort.Item.Submarine.Info.IsBeacon)
1205  {
1206  // specifically checking for friendlyNPC seems more logical here
1207  if (dockingPort.Item.Submarine.TeamID != item.Submarine.TeamID && dockingPort.Item.Submarine.TeamID != CharacterTeamType.FriendlyNPC) { continue; }
1208  }
1209 
1210  Vector2 offset = (dockingPort.Item.WorldPosition - transducerCenter) * scale;
1211  offset.Y = -offset.Y;
1212  if (offset.LengthSquared() > DisplayRadius * DisplayRadius) { continue; }
1213  Vector2 size = dockingPort.Item.Rect.Size.ToVector2() * scale;
1214 
1215  if (dockingPort.IsHorizontal)
1216  {
1217  size.X = 0.0f;
1218  }
1219  else
1220  {
1221  size.Y = 0.0f;
1222  }
1223  GUI.DrawLine(spriteBatch, center + offset - size - Vector2.Normalize(size) * zoom, center + offset + size + Vector2.Normalize(size) * zoom, Color.Black * signalStrength * 0.5f, width: (int)(zoom * 5.0f));
1224  GUI.DrawLine(spriteBatch, center + offset - size, center + offset + size, positiveColor * signalStrength, width: (int)(zoom * 2.5f));
1225  }
1226  }
1227 
1228  private void DrawDockingIndicator(SpriteBatch spriteBatch, Steering steering, ref Vector2 transducerCenter)
1229  {
1230  float scale = DisplayScale;
1231 
1232  Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f;
1233  worldFocusPos.X = steering.DockingTarget.Item.WorldPosition.X;
1234 
1235  Vector2 sourcePortDiff = (steering.ActiveDockingSource.Item.WorldPosition - transducerCenter) * scale;
1236  Vector2 sourcePortPos = new Vector2(sourcePortDiff.X, -sourcePortDiff.Y);
1237  Vector2 targetPortDiff = (steering.DockingTarget.Item.WorldPosition - transducerCenter) * scale;
1238  Vector2 targetPortPos = new Vector2(targetPortDiff.X, -targetPortDiff.Y);
1239 
1240  System.Diagnostics.Debug.Assert(steering.ActiveDockingSource.IsHorizontal == steering.DockingTarget.IsHorizontal);
1241  Vector2 diff = steering.DockingTarget.Item.WorldPosition - steering.ActiveDockingSource.Item.WorldPosition;
1242  float dist = diff.Length();
1243  bool readyToDock =
1244  Math.Abs(diff.X) < steering.DockingTarget.DistanceTolerance.X &&
1245  Math.Abs(diff.Y) < steering.DockingTarget.DistanceTolerance.Y;
1246 
1247  Vector2 dockingDir = sourcePortPos - targetPortPos;
1248  Vector2 normalizedDockingDir = Vector2.Normalize(dockingDir);
1249  if (!dynamicDockingIndicator)
1250  {
1251  if (steering.ActiveDockingSource.IsHorizontal)
1252  {
1253  normalizedDockingDir = new Vector2(Math.Sign(normalizedDockingDir.X), 0.0f);
1254  }
1255  else
1256  {
1257  normalizedDockingDir = new Vector2(0.0f, Math.Sign(normalizedDockingDir.Y));
1258  }
1259  }
1260 
1261  Color staticLineColor = Color.White * 0.2f;
1262 
1263  float sector = MathHelper.ToRadians(MathHelper.Lerp(10.0f, 45.0f, MathHelper.Clamp(dist / steering.DockingAssistThreshold, 0.0f, 1.0f)));
1264  float sectorLength = DisplayRadius;
1265  //use law of cosines to calculate the length of the center line
1266  float midLength = (float)(Math.Cos(sector) * sectorLength);
1267 
1268  Vector2 midNormal = new Vector2(-normalizedDockingDir.Y, normalizedDockingDir.X);
1269 
1270  DrawLine(spriteBatch, targetPortPos, targetPortPos + normalizedDockingDir * midLength, readyToDock ? positiveColor : staticLineColor, width: 2);
1271  DrawLine(spriteBatch, targetPortPos,
1272  targetPortPos + MathUtils.RotatePoint(normalizedDockingDir, sector) * sectorLength, staticLineColor, width: 2);
1273  DrawLine(spriteBatch, targetPortPos,
1274  targetPortPos + MathUtils.RotatePoint(normalizedDockingDir, -sector) * sectorLength, staticLineColor, width: 2);
1275 
1276  for (float z = 0; z < 1.0f; z += 0.1f * zoom)
1277  {
1278  Vector2 linePos = targetPortPos + normalizedDockingDir * midLength * z;
1279  DrawLine(spriteBatch, linePos + midNormal * 3.0f, linePos - midNormal * 3.0f, staticLineColor, width: 3);
1280  }
1281 
1282  if (readyToDock)
1283  {
1284  Color indicatorColor = positiveColor * 0.8f;
1285 
1286  float indicatorSize = (float)Math.Sin((float)Timing.TotalTime * 5.0f) * DisplayRadius * 0.75f;
1287  Vector2 midPoint = (sourcePortPos + targetPortPos) / 2.0f;
1288  DrawLine(spriteBatch,
1289  midPoint + Vector2.UnitY * indicatorSize,
1290  midPoint - Vector2.UnitY * indicatorSize,
1291  indicatorColor, width: 3);
1292  DrawLine(spriteBatch,
1293  midPoint + Vector2.UnitX * indicatorSize,
1294  midPoint - Vector2.UnitX * indicatorSize,
1295  indicatorColor, width: 3);
1296  }
1297  else
1298  {
1299  float indicatorSector = sector * 0.75f;
1300  float indicatorSectorLength = (float)(midLength / Math.Cos(indicatorSector));
1301 
1302  bool withinSector =
1303  (Math.Abs(diff.X) < steering.ActiveDockingSource.DistanceTolerance.X && Math.Abs(diff.Y) < steering.ActiveDockingSource.DistanceTolerance.Y) ||
1304  Vector2.Dot(normalizedDockingDir, MathUtils.RotatePoint(normalizedDockingDir, indicatorSector)) <
1305  Vector2.Dot(normalizedDockingDir, Vector2.Normalize(dockingDir));
1306 
1307  Color indicatorColor = withinSector ? positiveColor : negativeColor;
1308  indicatorColor *= 0.8f;
1309 
1310  DrawLine(spriteBatch, targetPortPos,
1311  targetPortPos + MathUtils.RotatePoint(normalizedDockingDir,indicatorSector) * indicatorSectorLength, indicatorColor, width: 3);
1312  DrawLine(spriteBatch, targetPortPos,
1313  targetPortPos + MathUtils.RotatePoint(normalizedDockingDir, -indicatorSector) * indicatorSectorLength, indicatorColor, width: 3);
1314  }
1315 
1316  }
1317 
1318  private void UpdateDisruptions(Vector2 pingSource, float worldPingRadius)
1319  {
1320  float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
1321 
1322  disruptedDirections.Clear();
1323 
1324  for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
1325  {
1326  foreach (LevelObject levelObject in nearbyObjects)
1327  {
1328  if (levelObject.ActivePrefab?.SonarDisruption <= 0.0f) { continue; }
1329 
1330  float disruptionStrength = levelObject.ActivePrefab.SonarDisruption;
1331  Vector2 disruptionPos = new Vector2(levelObject.Position.X, levelObject.Position.Y);
1332 
1333  float disruptionDist = Vector2.Distance(pingSource, disruptionPos);
1334  disruptedDirections.Add(((disruptionPos - pingSource) / disruptionDist, disruptionStrength));
1335 
1336  CreateBlipsForDisruption(disruptionPos, disruptionStrength);
1337 
1338  }
1339  foreach (AITarget aiTarget in AITarget.List)
1340  {
1341  float disruption = aiTarget.Entity is Character c && !c.IsUnconscious ? c.Params.SonarDisruption : aiTarget.SonarDisruption;
1342  if (disruption <= 0.0f || aiTarget.InDetectable) { continue; }
1343  float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource);
1344  if (distSqr > worldPingRadiusSqr) { continue; }
1345  float disruptionDist = (float)Math.Sqrt(distSqr);
1346  disruptedDirections.Add(((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption));
1347  CreateBlipsForDisruption(aiTarget.WorldPosition, disruption);
1348  }
1349  }
1350 
1351  void CreateBlipsForDisruption(Vector2 disruptionPos, float disruptionStrength)
1352  {
1353  disruptionStrength = Math.Min(disruptionStrength, 10.0f);
1354  Vector2 dir = disruptionPos - pingSource;
1355  for (int i = 0; i < disruptionStrength * 10.0f; i++)
1356  {
1357  Vector2 pos = disruptionPos + Rand.Vector(Rand.Range(0.0f, Level.GridCellSize * 4 * disruptionStrength));
1358  if (Vector2.Dot(pos - pingSource, -dir) > 1.0f - disruptionStrength) { continue; }
1359  var blip = new SonarBlip(
1360  pos,
1361  MathHelper.Lerp(0.1f, 1.5f, Math.Min(disruptionStrength, 1.0f)),
1362  Rand.Range(0.2f, 1.0f + disruptionStrength),
1363  BlipType.Disruption);
1364  sonarBlips.Add(blip);
1365  }
1366  }
1367  }
1368 
1369  public void RegisterExplosion(Explosion explosion, Vector2 worldPosition)
1370  {
1371  if (Character.Controlled?.SelectedItem != item) { return; }
1372  if (explosion.Attack.StructureDamage <= 0 && explosion.Attack.ItemDamage <= 0 && explosion.EmpStrength <= 0) { return; }
1373  Vector2 transducerCenter = GetTransducerPos();
1374  if (Vector2.DistanceSquared(worldPosition, transducerCenter) > range * range) { return; }
1375  int blipCount = MathHelper.Clamp((int)(explosion.Attack.Range / 100.0f), 0, 50);
1376  for (int i = 0; i < blipCount; i++)
1377  {
1378  sonarBlips.Add(new SonarBlip(
1379  worldPosition + Rand.Vector(Rand.Range(0.0f, explosion.Attack.Range)),
1380  1.0f,
1381  Rand.Range(0.5f, 1.0f),
1382  BlipType.Disruption));
1383  }
1384  if (explosion.EmpStrength > 0.0f)
1385  {
1386  int empBlipCount = MathHelper.Clamp((int)(blipCount * explosion.EmpStrength), 10, 50);
1387  for (int i = 0; i < empBlipCount; i++)
1388  {
1389  Vector2 dir = Rand.Vector(1.0f);
1390  var longRangeBlip = new SonarBlip(worldPosition, Rand.Range(1.9f, 2.1f), Rand.Range(1.0f, 1.5f), BlipType.LongRange)
1391  {
1392  Velocity = dir * MathUtils.Round(Rand.Range(4000.0f, 6000.0f), 1000.0f),
1393  Rotation = (float)Math.Atan2(-dir.Y, dir.X)
1394  };
1395  longRangeBlip.Size.Y *= 4.0f;
1396  sonarBlips.Add(longRangeBlip);
1397  }
1398  }
1399  }
1400 
1401  private void Ping(Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float displayScale, float range, bool passive,
1402  float pingStrength = 1.0f, AITarget needsToBeInSector = null)
1403  {
1404  float prevPingRadiusSqr = prevPingRadius * prevPingRadius;
1405  float pingRadiusSqr = pingRadius * pingRadius;
1406 
1407  //inside a hull -> only show the edges of the hull
1408  if (item.CurrentHull != null && DetectSubmarineWalls)
1409  {
1410  CreateBlipsForLine(
1411  new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y),
1412  new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y),
1413  pingSource, transducerPos,
1414  pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
1415 
1416  CreateBlipsForLine(
1418  new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
1419  pingSource, transducerPos,
1420  pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
1421 
1422  CreateBlipsForLine(
1423  new Vector2(item.CurrentHull.WorldRect.X, item.CurrentHull.WorldRect.Y),
1425  pingSource, transducerPos,
1426  pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
1427 
1428  CreateBlipsForLine(
1429  new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y),
1430  new Vector2(item.CurrentHull.WorldRect.Right, item.CurrentHull.WorldRect.Y - item.CurrentHull.Rect.Height),
1431  pingSource, transducerPos,
1432  pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, needsToBeInSector: needsToBeInSector);
1433 
1434  return;
1435  }
1436 
1437  foreach (Submarine submarine in Submarine.Loaded)
1438  {
1439  if (submarine.HullVertices == null) { continue; }
1440  if (!DetectSubmarineWalls)
1441  {
1442  if (connectedSubs.Contains(submarine)) { continue; }
1443  }
1444 
1445  //display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above)
1446  //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts
1447  Rectangle worldBorders = submarine.GetDockedBorders();
1448  worldBorders.Location += submarine.WorldPosition.ToPoint();
1449  if (Submarine.RectContains(worldBorders, pingSource) || submarine.Info.OutpostGenerationParams is { AlwaysShowStructuresOnSonar: true })
1450  {
1451  CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive);
1452  continue;
1453  }
1454 
1455  for (int i = 0; i < submarine.HullVertices.Count; i++)
1456  {
1457  Vector2 start = ConvertUnits.ToDisplayUnits(submarine.HullVertices[i]);
1458  Vector2 end = ConvertUnits.ToDisplayUnits(submarine.HullVertices[(i + 1) % submarine.HullVertices.Count]);
1459 
1460  if (item.Submarine == submarine)
1461  {
1462  start += Rand.Vector(500.0f);
1463  end += Rand.Vector(500.0f);
1464  }
1465 
1466  CreateBlipsForLine(
1467  start + submarine.WorldPosition,
1468  end + submarine.WorldPosition,
1469  pingSource, transducerPos,
1470  pingRadius, prevPingRadius,
1471  200.0f, 2.0f, range, 1.0f, passive,
1472  needsToBeInSector: needsToBeInSector);
1473  }
1474  }
1475 
1476  if (Level.Loaded != null && (item.CurrentHull == null || !DetectSubmarineWalls))
1477  {
1478  if (Level.Loaded.Size.Y - pingSource.Y < range)
1479  {
1480  CreateBlipsForLine(
1481  new Vector2(pingSource.X - range, Level.Loaded.Size.Y),
1482  new Vector2(pingSource.X + range, Level.Loaded.Size.Y),
1483  pingSource, transducerPos,
1484  pingRadius, prevPingRadius,
1485  250.0f, 150.0f, range, pingStrength, passive,
1486  needsToBeInSector: needsToBeInSector);
1487  }
1488  if (pingSource.Y - Level.Loaded.BottomPos < range)
1489  {
1490  CreateBlipsForLine(
1491  new Vector2(pingSource.X - range, Level.Loaded.BottomPos),
1492  new Vector2(pingSource.X + range, Level.Loaded.BottomPos),
1493  pingSource, transducerPos,
1494  pingRadius, prevPingRadius,
1495  250.0f, 150.0f, range, pingStrength, passive,
1496  needsToBeInSector: needsToBeInSector);
1497  }
1498 
1499  List<Voronoi2.VoronoiCell> cells = Level.Loaded.GetCells(pingSource, 7);
1500  foreach (Voronoi2.VoronoiCell cell in cells)
1501  {
1502  foreach (Voronoi2.GraphEdge edge in cell.Edges)
1503  {
1504  if (!edge.IsSolid) { continue; }
1505 
1506  //the normal of the edge must be pointing towards the ping source to be visible
1507  float cellDot = Vector2.Dot((edge.Center + cell.Translation) - pingSource, edge.GetNormal(cell));
1508  if (cellDot > 0) { continue; }
1509 
1510  float facingDot = Vector2.Dot(
1511  Vector2.Normalize(edge.Point1 - edge.Point2),
1512  Vector2.Normalize(cell.Center - pingSource));
1513 
1514  CreateBlipsForLine(
1515  edge.Point1 + cell.Translation,
1516  edge.Point2 + cell.Translation,
1517  pingSource, transducerPos,
1518  pingRadius, prevPingRadius,
1519  350.0f, 3.0f * (Math.Abs(facingDot) + 1.0f), range, pingStrength, passive,
1520  blipType : cell.IsDestructible ? BlipType.Destructible : BlipType.Default,
1521  needsToBeInSector: needsToBeInSector);
1522  }
1523  }
1524  }
1525 
1526  foreach (Item item in Item.SonarVisibleItems)
1527  {
1528  System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f);
1529  if (item.CurrentHull == null)
1530  {
1531  float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
1532  if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
1533  {
1534  var blip = new SonarBlip(
1535  item.WorldPosition + Rand.Vector(item.Prefab.SonarSize),
1536  MathHelper.Clamp(item.Prefab.SonarSize, 0.1f, pingStrength),
1537  MathHelper.Clamp(item.Prefab.SonarSize * 0.1f, 0.1f, 10.0f));
1538  if (!IsVisible(blip)) { continue; }
1539  sonarBlips.Add(blip);
1540  }
1541  }
1542  }
1543 
1544  foreach (Character c in Character.CharacterList)
1545  {
1546  if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; }
1547  if (!c.IsUnconscious && c.Params.HideInSonar) { continue; }
1548  if (c.InDetectable) { continue; }
1549  if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; }
1550 
1551  if (c.AnimController.SimplePhysicsEnabled)
1552  {
1553  float pointDist = ((c.WorldPosition - pingSource) * displayScale).LengthSquared();
1554  if (pointDist > DisplayRadius * DisplayRadius) { continue; }
1555 
1556  if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
1557  {
1558  var blip = new SonarBlip(
1559  c.WorldPosition,
1560  MathHelper.Clamp(c.Mass, 0.1f, pingStrength),
1561  MathHelper.Clamp(c.Mass * 0.03f, 0.1f, 2.0f));
1562  if (!IsVisible(blip)) { continue; }
1563  sonarBlips.Add(blip);
1564  HintManager.OnSonarSpottedCharacter(Item, c);
1565  }
1566  continue;
1567  }
1568 
1569  foreach (Limb limb in c.AnimController.Limbs)
1570  {
1571  if (!limb.body.Enabled) { continue; }
1572 
1573  float pointDist = ((limb.WorldPosition - pingSource) * displayScale).LengthSquared();
1574  if (limb.SimPosition == Vector2.Zero || pointDist > DisplayRadius * DisplayRadius) { continue; }
1575 
1576  if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
1577  {
1578  var blip = new SonarBlip(
1579  limb.WorldPosition + Rand.Vector(limb.Mass / 10.0f),
1580  MathHelper.Clamp(limb.Mass, 0.1f, pingStrength),
1581  MathHelper.Clamp(limb.Mass * 0.1f, 0.1f, 2.0f));
1582  if (!IsVisible(blip)) { continue; }
1583  sonarBlips.Add(blip);
1584  HintManager.OnSonarSpottedCharacter(Item, c);
1585  }
1586  }
1587  }
1588 
1589  bool IsVisible(SonarBlip blip)
1590  {
1591  if (!passive && !CheckBlipVisibility(blip, transducerPos)) { return false; }
1592  if (needsToBeInSector != null)
1593  {
1594  if (!needsToBeInSector.IsWithinSector(blip.Position)) { return false; }
1595  }
1596  return true;
1597  }
1598  }
1599 
1600  private void CreateBlipsForLine(Vector2 point1, Vector2 point2, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius,
1601  float lineStep, float zStep, float range, float pingStrength, bool passive, BlipType blipType = BlipType.Default, AITarget needsToBeInSector = null)
1602  {
1603  lineStep /= zoom;
1604  zStep /= zoom;
1605  range *= DisplayScale;
1606  float length = (point1 - point2).Length();
1607  Vector2 lineDir = (point2 - point1) / length;
1608  for (float x = 0; x < length; x += lineStep * Rand.Range(0.8f, 1.2f))
1609  {
1610  if (Rand.Int(sonarBlips.Count) > 500) { continue; }
1611 
1612  Vector2 point = point1 + lineDir * x;
1613 
1614  //ignore if outside the display
1615  Vector2 transducerDiff = point - transducerPos;
1616  Vector2 transducerDisplayDiff = transducerDiff * DisplayScale / zoom;
1617  if (transducerDisplayDiff.LengthSquared() > DisplayRadius * DisplayRadius) { continue; }
1618 
1619  //ignore if the point is not within the ping
1620  Vector2 pointDiff = point - pingSource;
1621  Vector2 displayPointDiff = pointDiff * DisplayScale / zoom;
1622  float displayPointDistSqr = displayPointDiff.LengthSquared();
1623  if (displayPointDistSqr < prevPingRadius * prevPingRadius || displayPointDistSqr > pingRadius * pingRadius) { continue; }
1624 
1625  //ignore if direction is disrupted
1626  float transducerDist = transducerDiff.Length();
1627  Vector2 pingDirection = transducerDiff / transducerDist;
1628  bool disrupted = false;
1629  foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections)
1630  {
1631  float dot = Vector2.Dot(pingDirection, disruptPos);
1632  if (dot > 1.0f - disruptStrength)
1633  {
1634  disrupted = true;
1635  break;
1636  }
1637  }
1638  if (disrupted) { continue; }
1639 
1640  float displayPointDist = (float)Math.Sqrt(displayPointDistSqr);
1641  float alpha = pingStrength * Rand.Range(1.5f, 2.0f);
1642  for (float z = 0; z < DisplayRadius - transducerDist * DisplayScale; z += zStep)
1643  {
1644  Vector2 pos = point + Rand.Vector(150.0f / zoom) + pingDirection * z / DisplayScale;
1645  float fadeTimer = alpha * (1.0f - displayPointDist / range);
1646 
1647  if (needsToBeInSector != null)
1648  {
1649  if (!needsToBeInSector.IsWithinSector(pos)) { continue; }
1650  }
1651 
1652  var blip = new SonarBlip(pos, fadeTimer, 1.0f + ((displayPointDist + z) / DisplayRadius), blipType);
1653  if (!passive && !CheckBlipVisibility(blip, transducerPos)) { continue; }
1654 
1655  int minDist = (int)(200 / zoom);
1656  sonarBlips.RemoveAll(b => b.FadeTimer < fadeTimer && Math.Abs(pos.X - b.Position.X) < minDist && Math.Abs(pos.Y - b.Position.Y) < minDist);
1657 
1658  sonarBlips.Add(blip);
1659  zStep += 0.5f / zoom;
1660 
1661  if (z == 0)
1662  {
1663  alpha = Math.Min(alpha - 0.5f, 1.5f);
1664  }
1665  else
1666  {
1667  alpha -= 0.1f;
1668  }
1669 
1670  if (alpha < 0) { break; }
1671  }
1672  }
1673  }
1674 
1675  private void CreateBlipsForSubmarineWalls(Submarine sub, Vector2 pingSource, Vector2 transducerPos, float pingRadius, float prevPingRadius, float range, bool passive)
1676  {
1677  foreach (Structure structure in Structure.WallList)
1678  {
1679  if (structure.Submarine != sub) { continue; }
1680  CreateBlips(structure.IsHorizontal, structure.WorldPosition, structure.WorldRect);
1681  }
1682  foreach (var door in Door.DoorList)
1683  {
1684  if (door.Item.Submarine != sub || door.IsOpen) { continue; }
1685  CreateBlips(door.IsHorizontal, door.Item.WorldPosition, door.Item.WorldRect, BlipType.Door);
1686  }
1687 
1688  void CreateBlips(bool isHorizontal, Vector2 worldPos, Rectangle worldRect, BlipType blipType = BlipType.Default)
1689  {
1690  Vector2 point1, point2;
1691  if (isHorizontal)
1692  {
1693  point1 = new Vector2(worldRect.X, worldPos.Y);
1694  point2 = new Vector2(worldRect.Right, worldPos.Y);
1695  }
1696  else
1697  {
1698  point1 = new Vector2(worldPos.X, worldRect.Y);
1699  point2 = new Vector2(worldPos.X, worldRect.Y - worldRect.Height);
1700  }
1701  CreateBlipsForLine(
1702  point1,
1703  point2,
1704  pingSource, transducerPos,
1705  pingRadius, prevPingRadius, 50.0f, 5.0f, range, 2.0f, passive, blipType);
1706  }
1707  }
1708 
1709  private bool CheckBlipVisibility(SonarBlip blip, Vector2 transducerPos)
1710  {
1711  Vector2 pos = (blip.Position - transducerPos) * DisplayScale;
1712  pos.Y = -pos.Y;
1713 
1714  float posDistSqr = pos.LengthSquared();
1715  if (posDistSqr > DisplayRadius * DisplayRadius)
1716  {
1717  blip.FadeTimer = 0.0f;
1718  return false;
1719  }
1720 
1721  Vector2 dir = pos / (float)Math.Sqrt(posDistSqr);
1722  if (currentPingIndex != -1 && activePings[currentPingIndex].IsDirectional)
1723  {
1724  if (Vector2.Dot(activePings[currentPingIndex].Direction, dir) < DirectionalPingDotProduct)
1725  {
1726  blip.FadeTimer = 0.0f;
1727  return false;
1728  }
1729  }
1730  return true;
1731  }
1732 
1736  private bool CheckResourceMarkerVisibility(Vector2 resourcePos, Vector2 transducerPos)
1737  {
1738  var distSquared = Vector2.DistanceSquared(transducerPos, resourcePos);
1739  if (distSquared > Range * Range)
1740  {
1741  return false;
1742  }
1743  if (currentPingIndex != -1 && activePings[currentPingIndex].IsDirectional)
1744  {
1745  var pos = (resourcePos - transducerPos) * DisplayScale;
1746  pos.Y = -pos.Y;
1747  var length = pos.Length();
1748  var dir = pos / length;
1749  if (Vector2.Dot(activePings[currentPingIndex].Direction, dir) < DirectionalPingDotProduct)
1750  {
1751  return false;
1752  }
1753  }
1754  return true;
1755  }
1756 
1757  private void DrawBlip(SpriteBatch spriteBatch, SonarBlip blip, Vector2 transducerPos, Vector2 center, float strength, float blipScale)
1758  {
1759  strength = MathHelper.Clamp(strength, 0.0f, 1.0f);
1760 
1761  float distort = 1.0f - item.Condition / item.MaxCondition;
1762 
1763  Vector2 pos = (blip.Position - transducerPos) * DisplayScale;
1764  pos.Y = -pos.Y;
1765 
1766  if (Rand.Range(0.5f, 2.0f) < distort) { pos.X = -pos.X; }
1767  if (Rand.Range(0.5f, 2.0f) < distort) { pos.Y = -pos.Y; }
1768 
1769  float posDistSqr = pos.LengthSquared();
1770  if (posDistSqr > DisplayRadius * DisplayRadius)
1771  {
1772  blip.FadeTimer = 0.0f;
1773  return;
1774  }
1775 
1776  if (sonarBlip == null)
1777  {
1778  GUI.DrawRectangle(spriteBatch, center + pos, Vector2.One * 4, Color.Magenta, true);
1779  return;
1780  }
1781 
1782  Vector2 dir = pos / (float)Math.Sqrt(posDistSqr);
1783  Vector2 normal = new Vector2(dir.Y, -dir.X);
1784  float scale = (strength + 3.0f) * blip.Scale * blipScale;
1785  Color color = ToolBox.GradientLerp(strength, blipColorGradient[blip.BlipType]);
1786 
1787  sonarBlip.Draw(spriteBatch, center + pos, color * blip.Alpha, sonarBlip.Origin, blip.Rotation ?? MathUtils.VectorToAngle(pos),
1788  blip.Size * scale * 0.5f, SpriteEffects.None, 0);
1789 
1790  pos += Rand.Range(0.0f, 1.0f) * dir + Rand.Range(-scale, scale) * normal;
1791 
1792  sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f * blip.Alpha, sonarBlip.Origin, 0, scale, SpriteEffects.None, 0);
1793  }
1794 
1795  private void DrawMarker(SpriteBatch spriteBatch, string label, Identifier iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius,
1796  bool onlyShowTextOnMouseOver = false)
1797  {
1798  float linearDist = Vector2.Distance(worldPosition, transducerPosition);
1799  float dist = linearDist;
1800  if (linearDist > Range)
1801  {
1802  if (markerDistances.TryGetValue(targetIdentifier, out CachedDistance cachedDistance))
1803  {
1804  if (cachedDistance.ShouldUpdateDistance(transducerPosition, worldPosition))
1805  {
1806  markerDistances.Remove(targetIdentifier);
1807  CalculateDistance();
1808  }
1809  else
1810  {
1811  dist = Math.Max(cachedDistance.Distance, linearDist);
1812  }
1813  }
1814  else
1815  {
1816  CalculateDistance();
1817  }
1818  }
1819 
1820  void CalculateDistance()
1821  {
1822  pathFinder ??= new PathFinder(WayPoint.WayPointList, false);
1823  var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition));
1824  if (!path.Unreachable)
1825  {
1826  var cachedDistance = new CachedDistance(transducerPosition, worldPosition, path.TotalLength, Timing.TotalTime + Rand.Range(1.0f, 5.0f));
1827  markerDistances.Add(targetIdentifier, cachedDistance);
1828  dist = path.TotalLength;
1829  }
1830  else
1831  {
1832  var cachedDistance = new CachedDistance(transducerPosition, worldPosition, linearDist, Timing.TotalTime + Rand.Range(4.0f, 7.0f));
1833  markerDistances.Add(targetIdentifier, cachedDistance);
1834  }
1835  }
1836 
1837  Vector2 position = worldPosition - transducerPosition;
1838 
1839  position *= scale;
1840  position.Y = -position.Y;
1841 
1842  float textAlpha = MathHelper.Clamp(1.5f - dist / 50000.0f, 0.5f, 1.0f);
1843 
1844  Vector2 dir = Vector2.Normalize(position);
1845  Vector2 markerPos = (linearDist * scale > radius) ? dir * radius : position;
1846  markerPos += center;
1847 
1848  markerPos.X = (int)markerPos.X;
1849  markerPos.Y = (int)markerPos.Y;
1850 
1851  float alpha = 1.0f;
1852  if (!onlyShowTextOnMouseOver)
1853  {
1854  if (linearDist * scale < radius)
1855  {
1856  float normalizedDist = linearDist * scale / radius;
1857  alpha = Math.Max(normalizedDist - 0.4f, 0.0f);
1858 
1859  float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos);
1860  float hoverThreshold = 150.0f;
1861  if (mouseDist < hoverThreshold)
1862  {
1863  alpha += (hoverThreshold - mouseDist) / hoverThreshold;
1864  }
1865  }
1866  }
1867  else
1868  {
1869  float mouseDist = Vector2.Distance(PlayerInput.MousePosition, markerPos);
1870  if (mouseDist > 5)
1871  {
1872  alpha = 0.0f;
1873  }
1874  }
1875 
1876  if (iconIdentifier == null || !targetIcons.TryGetValue(iconIdentifier, out var iconInfo) || iconInfo.Item1 == null)
1877  {
1878  GUI.DrawRectangle(spriteBatch, new Rectangle((int)markerPos.X - 3, (int)markerPos.Y - 3, 6, 6), markerColor, thickness: 2);
1879  }
1880  else
1881  {
1882  iconInfo.Item1.Draw(spriteBatch, markerPos, iconInfo.Item2);
1883  }
1884 
1885  if (alpha <= 0.0f) { return; }
1886 
1887  string wrappedLabel = ToolBox.WrapText(label, 150, GUIStyle.SmallFont.Value);
1888  wrappedLabel += "\n" + ((int)(dist * Physics.DisplayToRealWorldRatio) + " m");
1889 
1890  Vector2 labelPos = markerPos;
1891  Vector2 textSize = GUIStyle.SmallFont.MeasureString(wrappedLabel);
1892 
1893  //flip the text to left side when the marker is on the left side or goes outside the right edge of the interface
1894  if (GuiFrame != null && (dir.X < 0.0f || labelPos.X + textSize.X + 10 > GuiFrame.Rect.X) && labelPos.X - textSize.X > 0)
1895  {
1896  labelPos.X -= textSize.X + 10;
1897  }
1898 
1899  GUI.DrawString(spriteBatch,
1900  new Vector2(labelPos.X + 10, labelPos.Y),
1901  wrappedLabel,
1902  Color.LightBlue * textAlpha * alpha, Color.Black * textAlpha * 0.8f * alpha,
1903  2, GUIStyle.SmallFont);
1904  }
1905 
1906  protected override void RemoveComponentSpecific()
1907  {
1908  base.RemoveComponentSpecific();
1909  sonarBlip?.Remove();
1910  pingCircle?.Remove();
1911  directionalPingCircle?.Remove();
1912  screenOverlay?.Remove();
1913  screenBackground?.Remove();
1914  lineSprite?.Remove();
1915 
1916  foreach (var t in targetIcons.Values)
1917  {
1918  t.Item1.Remove();
1919  }
1920  targetIcons.Clear();
1921 
1922  MineralClusters = null;
1923  }
1924 
1925  public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
1926  {
1927  msg.WriteBoolean(currentMode == Mode.Active);
1928  if (currentMode == Mode.Active)
1929  {
1930  msg.WriteRangedSingle(zoom, MinZoom, MaxZoom, 8);
1931  msg.WriteBoolean(useDirectionalPing);
1932  if (useDirectionalPing)
1933  {
1934  float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
1935  msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
1936  }
1938  }
1939  }
1940 
1941  public void ClientEventRead(IReadMessage msg, float sendingTime)
1942  {
1943  int msgStartPos = msg.BitPosition;
1944 
1945  bool isActive = msg.ReadBoolean();
1946  float zoomT = 1.0f;
1947  bool directionalPing = useDirectionalPing;
1948  float directionT = 0.0f;
1949  bool mineralScanner = UseMineralScanner;
1950  if (isActive)
1951  {
1952  zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
1953  directionalPing = msg.ReadBoolean();
1954  if (directionalPing)
1955  {
1956  directionT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
1957  }
1958  mineralScanner = msg.ReadBoolean();
1959  }
1960 
1961  if (correctionTimer > 0.0f)
1962  {
1963  int msgLength = msg.BitPosition - msgStartPos;
1964  msg.BitPosition = msgStartPos;
1965  StartDelayedCorrection(msg.ExtractBits(msgLength), sendingTime);
1966  return;
1967  }
1968 
1969  CurrentMode = isActive ? Mode.Active : Mode.Passive;
1970  if (isActive)
1971  {
1972  zoomSlider.BarScroll = zoomT;
1973  zoom = MathHelper.Lerp(MinZoom, MaxZoom, zoomT);
1974  if (directionalPing)
1975  {
1976  float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, directionT);
1977  pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
1978  }
1979  useDirectionalPing = directionalModeSwitch.Selected = directionalPing;
1980  UseMineralScanner = mineralScanner;
1981  if (mineralScannerSwitch != null)
1982  {
1983  mineralScannerSwitch.Selected = mineralScanner;
1984  }
1985  }
1986  }
1987 
1988  private void UpdateGUIElements()
1989  {
1990  bool isActive = CurrentMode == Mode.Active;
1991  SonarModeSwitch.Selected = isActive;
1992  passiveTickBox.Selected = !isActive;
1993  activeTickBox.Selected = isActive;
1994  directionalModeSwitch.Selected = useDirectionalPing;
1995  if (mineralScannerSwitch != null)
1996  {
1997  mineralScannerSwitch.Selected = UseMineralScanner;
1998  }
1999  }
2000  }
2001 
2003  {
2004  public float FadeTimer;
2005  public Vector2 Position;
2006  public float Scale;
2007  public Vector2 Velocity;
2008  public float? Rotation;
2009  public Vector2 Size;
2011  public float Alpha = 1.0f;
2012 
2013  public SonarBlip(Vector2 pos, float fadeTimer, float scale, Sonar.BlipType blipType = Sonar.BlipType.Default)
2014  {
2015  Position = pos;
2016  FadeTimer = Math.Max(fadeTimer, 0.0f);
2017  Scale = scale;
2018  Size = new Vector2(0.5f, 1.0f);
2019  BlipType = blipType;
2020  }
2021  }
2022 }
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
Explosions are area of effect attacks that can damage characters, items and structures.
float EmpStrength
Strength of the EMP effect created by the explosion.
GUIComponent GetChild(int index)
Definition: GUIComponent.cs:54
bool IsParentOf(GUIComponent component, bool recursive=true)
Definition: GUIComponent.cs:75
GUIComponent GetChildByUserData(object obj)
Definition: GUIComponent.cs:66
virtual Rectangle Rect
RectTransform RectTransform
GUIComponent that can be used to render custom content on the UI
OnMovedHandler OnMoved
Definition: GUIScrollBar.cs:26
void OverrideTextColor(Color color)
Overrides the color for all the states.
static void AutoScaleAndNormalize(params GUITextBlock[] textBlocks)
Set the text scale of the GUITextBlocks so that they all use the same scale and can fit the text with...
GUITextBlock TextBlock
Definition: GUITickBox.cs:93
override bool Selected
Definition: GUITickBox.cs:18
static GameClient Client
Definition: GameMain.cs:188
static IReadOnlyCollection< Item > SonarVisibleItems
Items whose ItemPrefab.SonarSize is larger than 0
void StartDelayedCorrection(IReadMessage buffer, float sendingTime, bool waitForMidRoundSync=false)
SonarBlip(Vector2 pos, float fadeTimer, float scale, Sonar.BlipType blipType=Sonar.BlipType.Default)
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
float DisplayScale
Current scale of the display, taking zoom into account. In other words, the scaling factor of world c...
override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData=null)
override void CreateGUI()
Overload this method and implement. The method is automatically called when the resolution changes.
LocalizedString Fallback(LocalizedString fallback, bool useDefaultLanguageIfFound=true)
Use this text instead if the original text cannot be found.
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
void SetPosition(Anchor anchor, Pivot? pivot=null)
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
Vector2 RelativeSize
Relative to the parent rect.
void Resize(Point absoluteSize, bool resizeChildren=true)
RectTransform?? Parent
RectTransform(Vector2 relativeSize, RectTransform parent, Anchor anchor=Anchor.TopLeft, Pivot? pivot=null, Point? minSize=null, Point? maxSize=null, ScaleBasis scaleBasis=ScaleBasis.Normal)
Vector2 RelativeOffset
Defined as portions of the parent size. Also the direction of the offset is relative,...
Point?? MaxSize
Max size in pixels. Does not affect scaling.
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
Highlights an UI element of some kind. Generally used in tutorials.
Interface for entities that the clients can send events to the server
Single ReadRangedSingle(Single min, Single max, int bitCount)
Interface for entities that the server can send events to the clients
void WriteRangedSingle(Single val, Single min, Single max, int bitCount)
GUISoundType
Definition: GUI.cs:21