Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs
2 using FarseerPhysics;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Xml.Linq;
9 using Voronoi2;
10 
12 {
14  {
15  public const float AutopilotMinDistToPathNode = 30.0f;
16 
17  private const float AutopilotRayCastInterval = 0.5f;
18  private const float RecalculatePathInterval = 5.0f;
19 
20  private const float AutoPilotSteeringLerp = 0.1f;
21 
22  private const float AutoPilotMaxSpeed = 0.5f;
23  private const float AIPilotMaxSpeed = 1.0f;
24 
28  public const float PressureWarningThreshold = 500.0f;
29 
33  const float DefaultSteeringAdjustSpeed = 0.2f;
34 
35  private Vector2 targetVelocity;
36 
37  private Vector2 steeringInput;
38 
39  private bool autoPilot;
40 
41  private Vector2? posToMaintain;
42 
43  private SteeringPath steeringPath;
44 
45  private PathFinder pathFinder;
46 
47  private float networkUpdateTimer;
48  private bool unsentChanges;
49 
50  private float autopilotRayCastTimer;
51  private float autopilotRecalculatePathTimer;
52 
53  private Vector2 avoidStrength;
54 
55  private float neutralBallastLevel;
56 
57  private float steeringAdjustSpeed = 1.0f;
58 
59  private Character user;
60 
61  private Sonar sonar;
62 
63  private Submarine controlledSub;
64 
65  // AI interfacing
66  public Vector2 AITacticalTarget { get; set; }
67  public float AIRamTimer { get; set; }
68  bool navigateTactically; // this will be removed after rewriting steering to use an enum
69 
70  private bool showIceSpireWarning;
71 
72  private List<Submarine> connectedSubs = new List<Submarine>();
73  private const float ConnectedSubUpdateInterval = 1.0f;
74  float connectedSubUpdateTimer;
75 
76  private double lastReceivedSteeringSignalTime;
77 
78  public bool AutoPilot
79  {
80  get { return autoPilot; }
81  set
82  {
83  if (value == autoPilot) { return; }
84  autoPilot = value;
85 #if CLIENT
86  UpdateGUIElements();
87 #endif
88  if (autoPilot)
89  {
90  if (pathFinder == null)
91  {
92  pathFinder = new PathFinder(WayPoint.WayPointList, false)
93  {
94  GetNodePenalty = GetNodePenalty
95  };
96  }
97  MaintainPos = true;
98  if (posToMaintain == null)
99  {
100  posToMaintain = controlledSub != null ?
101  controlledSub.WorldPosition :
103  }
104  }
105  else
106  {
107  PosToMaintain = null;
108  MaintainPos = false;
109  LevelEndSelected = false;
110  LevelStartSelected = false;
111  }
112  }
113  }
114 
115  [Editable(0.0f, 1.0f, decimals: 4),
116  Serialize(0.5f, IsPropertySaveable.Yes, description: "How full the ballast tanks should be when the submarine is not being steered upwards/downwards."
117  + " Can be used to compensate if the ballast tanks are too large/small relative to the size of the submarine.")]
118  public float NeutralBallastLevel
119  {
120  get { return neutralBallastLevel; }
121  set
122  {
123  neutralBallastLevel = MathHelper.Clamp(value, 0.0f, 1.0f);
124  }
125  }
126 
127  [Serialize(1000.0f, IsPropertySaveable.Yes, description: "How close the docking port has to be to another docking port for the docking mode to become active.")]
129  {
130  get;
131  set;
132  }
133 
134  public Vector2 TargetVelocity
135  {
136  get { return targetVelocity; }
137  set
138  {
139  if (!MathUtils.IsValid(value))
140  {
141  if (!MathUtils.IsValid(targetVelocity))
142  {
143  targetVelocity = Vector2.Zero;
144  }
145  return;
146  }
147  targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
148  targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
149  }
150  }
151 
153  {
154  get => TargetVelocity.LengthSquared();
155  }
156 
157  public Vector2 SteeringInput
158  {
159  get { return steeringInput; }
160  set
161  {
162  if (!MathUtils.IsValid(value)) return;
163  steeringInput.X = MathHelper.Clamp(value.X, -100.0f, 100.0f);
164  steeringInput.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f);
165  }
166  }
167 
169  {
170  get { return steeringPath; }
171  }
172 
173  public Vector2? PosToMaintain
174  {
175  get { return posToMaintain; }
176  set { posToMaintain = value; }
177  }
178 
179  struct ObstacleDebugInfo
180  {
181  public Vector2 Point1;
182  public Vector2 Point2;
183 
184  public Vector2? Intersection;
185 
186  public float Dot;
187 
188  public Vector2 AvoidStrength;
189 
190  public ObstacleDebugInfo(GraphEdge edge, Vector2? intersection, float dot, Vector2 avoidStrength, Vector2 translation)
191  {
192  Point1 = edge.Point1 + translation;
193  Point2 = edge.Point2 + translation;
194  Intersection = intersection;
195  Dot = dot;
196  AvoidStrength = avoidStrength;
197  }
198  }
199 
200  //edge point 1, edge point 2, avoid strength
201  private List<ObstacleDebugInfo> debugDrawObstacles = new List<ObstacleDebugInfo>();
202 
203  #region Docking
204  public List<DockingPort> DockingSources = new List<DockingPort>();
205  private bool searchedConnectedDockingPort;
206 
207  private bool dockingModeEnabled;
208  public bool DockingModeEnabled
209  {
210  get { return UseAutoDocking && dockingModeEnabled; }
211  set { dockingModeEnabled = value; }
212  }
213 
214  public bool UseAutoDocking
215  {
216  get;
217  set;
218  } = true;
219 
220  private void FindConnectedDockingPort()
221  {
222  searchedConnectedDockingPort = true;
223  foreach (MapEntity linkedTo in item.linkedTo)
224  {
225  if (linkedTo is Item item)
226  {
227  var port = item.GetComponent<DockingPort>();
228  if (port != null)
229  {
230  DockingSources.Add(port);
231  }
232  }
233  }
234 
235  var dockingConnection = item.Connections.FirstOrDefault(c => c.Name == "toggle_docking");
236  if (dockingConnection != null)
237  {
238  var connectedPorts = item.GetConnectedComponentsRecursive<DockingPort>(dockingConnection, allowTraversingBackwards: false);
239  DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost));
240  }
241  }
242  #endregion
243 
245  : base(item, element)
246  {
247  IsActive = true;
248  InitProjSpecific(element);
249  }
250 
251  partial void InitProjSpecific(ContentXElement element);
252 
253  public override void OnItemLoaded()
254  {
255  base.OnItemLoaded();
256  sonar = item.GetComponent<Sonar>();
257  }
258 
259  public override bool Select(Character character)
260  {
261  if (!CanBeSelected) return false;
262 
263  user = character;
264  return true;
265  }
266 
267  public override void Update(float deltaTime, Camera cam)
268  {
269  if (!searchedConnectedDockingPort)
270  {
271  FindConnectedDockingPort();
272  }
273  networkUpdateTimer -= deltaTime;
274  if (unsentChanges)
275  {
276  if (networkUpdateTimer <= 0.0f)
277  {
278 #if CLIENT
279  if (GameMain.Client != null)
280  {
281  item.CreateClientEvent(this);
283  }
284 #endif
285 #if SERVER
286  item.CreateServerEvent(this);
287 #endif
288 
289  networkUpdateTimer = 0.1f;
290  unsentChanges = false;
291  }
292  }
293 
294  controlledSub = item.Submarine;
295  var sonar = item.GetComponent<Sonar>();
296  if (sonar != null && sonar.UseTransducers)
297  {
298  controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
299  }
300 
301  if (Voltage < MinVoltage) { return; }
302 
303  if (user != null && user.Removed)
304  {
305  user = null;
306  }
307 
308  ApplyStatusEffects(ActionType.OnActive, deltaTime);
309 
310  float userSkill = 0.0f;
311  if (user != null && controlledSub != null &&
312  (user.SelectedItem == item || item.linkedTo.Contains(user.SelectedItem)))
313  {
314  userSkill = user.GetSkillLevel("helm") / 100.0f;
315  }
316 
317  // override autopilot pathing while the AI rams, and go full speed ahead
318  if (AIRamTimer > 0f && controlledSub != null)
319  {
320  AIRamTimer -= deltaTime;
321  TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f);
322  }
323  else if (AutoPilot)
324  {
325  //signals override autopilot for a duration of one second
326  if (lastReceivedSteeringSignalTime < Timing.TotalTime - 1)
327  {
328  UpdateAutoPilot(deltaTime);
329  float throttle = 1.0f;
330  if (controlledSub != null)
331  {
332  //if the sub is heading in the correct direction, throttle the speed according to the user's skill
333  //if it's e.g. sinking due to extra water, don't throttle, but allow emptying up the ballast completely
334  throttle = MathHelper.Clamp(Vector2.Dot(controlledSub.Velocity, TargetVelocity) / 100.0f, 0.0f, 1.0f);
335  }
336  float maxSpeed = MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f;
337  TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(100.0f, maxSpeed, throttle));
338  }
339  }
340  else
341  {
342  showIceSpireWarning = false;
343  if (user != null && user.Info != null &&
344  user.SelectedItem == item)
345  {
346  IncreaseSkillLevel(user, deltaTime);
347  }
348 
349  Vector2 velocityDiff = steeringInput - targetVelocity;
350  if (velocityDiff != Vector2.Zero)
351  {
352  if (steeringAdjustSpeed >= 0.99f)
353  {
354  TargetVelocity = steeringInput;
355  }
356  else
357  {
358  float steeringChange = 1.0f / (1.0f - steeringAdjustSpeed);
359  steeringChange *= steeringChange * 10.0f;
360 
361  TargetVelocity += Vector2.Normalize(velocityDiff) *
362  Math.Min(steeringChange * deltaTime, velocityDiff.Length());
363  }
364  }
365  }
366 
367  float velX = targetVelocity.X;
368  if (controlledSub != null && controlledSub.FlippedX) { velX *= -1; }
369  item.SendSignal(new Signal(velX.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_x_out");
370 
371  float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f);
372  item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out");
373 
374  // converts the controlled sub's velocity to km/h and sends it.
375  if (controlledSub is { } sub)
376  {
377  item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.X * Physics.DisplayToRealWorldRatio) * 3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_x");
378  item.SendSignal(new Signal((ConvertUnits.ToDisplayUnits(sub.Velocity.Y * Physics.DisplayToRealWorldRatio) * -3.6f).ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_velocity_y");
379 
380  Vector2 pos = new Vector2(sub.WorldPosition.X * Physics.DisplayToRealWorldRatio, sub.RealWorldDepth);
381  if (sonar != null && sonar.UseTransducers && sonar.CenterOnTransducers && sonar.ConnectedTransducers.Any())
382  {
383  pos = Vector2.Zero;
384  foreach (var connectedTransducer in sonar.ConnectedTransducers)
385  {
386  pos += connectedTransducer.Item.WorldPosition;
387  }
388  pos /= sonar.ConnectedTransducers.Count();
389  pos = new Vector2(
390  pos.X * Physics.DisplayToRealWorldRatio,
391  Level.Loaded?.GetRealWorldDepth(pos.Y) ?? (-pos.Y * Physics.DisplayToRealWorldRatio));
392  }
393 
394  item.SendSignal(new Signal(pos.X.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_x");
395  item.SendSignal(new Signal(pos.Y.ToString("0.0000", CultureInfo.InvariantCulture), sender: user), "current_position_y");
396  }
397 
398  // if our tactical AI pilot has left, revert back to maintaining position
399  if (navigateTactically && (user == null || user.SelectedItem != item))
400  {
401  navigateTactically = false;
402  AIRamTimer = 0f;
403  SetMaintainPosition();
404  }
405  }
406 
407  private void IncreaseSkillLevel(Character user, float deltaTime)
408  {
409  if (controlledSub == null) { return; }
410  if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; }
411  if (user?.Info == null) { return; }
412  // Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck)
413  if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
414 
415  float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f);
416  user.Info.ApplySkillGain(Tags.HelmSkill,
417  SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime);
418  }
419 
420  private void UpdateAutoPilot(float deltaTime)
421  {
422  if (controlledSub == null) { return; }
423  if (posToMaintain != null)
424  {
425  Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain, 10.0f);
426  TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp);
427  showIceSpireWarning = false;
428  return;
429  }
430 
431  autopilotRayCastTimer -= deltaTime;
432  autopilotRecalculatePathTimer -= deltaTime;
433  if (autopilotRecalculatePathTimer <= 0.0f)
434  {
435  //periodically recalculate the path in case the sub ends up to a position
436  //where it can't keep traversing the initially calculated path
437  UpdatePath();
438  autopilotRecalculatePathTimer = RecalculatePathInterval;
439  }
440 
441  if (steeringPath == null)
442  {
443  showIceSpireWarning = false;
444  return;
445  }
446  steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f);
447 
448  connectedSubUpdateTimer -= deltaTime;
449  if (connectedSubUpdateTimer <= 0.0f)
450  {
451  connectedSubs.Clear();
452  connectedSubs.AddRange(controlledSub.GetConnectedSubs());
453  connectedSubUpdateTimer = ConnectedSubUpdateInterval;
454  }
455 
456  if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null)
457  {
458  Vector2 diff = ConvertUnits.ToSimUnits(steeringPath.NextNode.Position - controlledSub.WorldPosition);
459 
460  //if the node is close enough, check if it's visible
461  float lengthSqr = diff.LengthSquared();
462  if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode)
463  {
464  diff = Vector2.Normalize(diff);
465 
466  //check if the next waypoint is visible from all corners of the sub
467  //(i.e. if we can navigate directly towards it or if there's obstacles in the way)
468  bool nextVisible = true;
469  for (int x = -1; x < 2; x += 2)
470  {
471  for (int y = -1; y < 2; y += 2)
472  {
473  Vector2 cornerPos =
474  new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f;
475 
476  cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition);
477 
478  float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition);
479 
480  if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; }
481 
482  nextVisible = false;
483  x = 2;
484  y = 2;
485  }
486  }
487 
488  if (nextVisible) steeringPath.SkipToNextNode();
489  }
490 
491  autopilotRayCastTimer = AutopilotRayCastInterval;
492  }
493 
494  Vector2 newVelocity = Vector2.Zero;
495  if (steeringPath.CurrentNode != null)
496  {
497  newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition, 2.0f);
498  }
499 
500  Vector2 avoidDist = new Vector2(
501  Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f),
502  Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f));
503 
504  float avoidRadius = avoidDist.Length();
505  float damagingWallAvoidRadius = MathHelper.Clamp(avoidRadius * 1.5f, 5000.0f, 10000.0f);
506 
507  Vector2 newAvoidStrength = Vector2.Zero;
508 
509  debugDrawObstacles.Clear();
510 
511  //steer away from nearby walls
512  showIceSpireWarning = false;
513  var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
514  foreach (VoronoiCell cell in closeCells)
515  {
516  if (cell.DoesDamage || cell.Body is { BodyType: BodyType.Dynamic })
517  {
518  foreach (GraphEdge edge in cell.Edges)
519  {
520  Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition);
521  Vector2 diff = closestPoint - controlledSub.WorldPosition;
522  float dist = diff.Length() - Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height) / 2;
523  if (dist > damagingWallAvoidRadius) { continue; }
524 
525  Vector2 normalizedDiff = Vector2.Normalize(diff);
526  float dot = Vector2.Dot(normalizedDiff, controlledSub.Velocity);
527 
528  float avoidStrength = MathHelper.Clamp(MathHelper.Lerp(1.0f, 0.0f, dist / damagingWallAvoidRadius - dot), 0.0f, 1.0f);
529  Vector2 avoid = -normalizedDiff * avoidStrength;
530  newAvoidStrength += avoid;
531  debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation));
532 
533  if (dot > 0.0f && cell.DoesDamage)
534  {
535  showIceSpireWarning = true;
536  }
537  }
538  continue;
539  }
540 
541  foreach (GraphEdge edge in cell.Edges)
542  {
543  if (MathUtils.GetLineSegmentIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
544  {
545  Vector2 diff = controlledSub.WorldPosition - intersection;
546  //far enough -> ignore
547  if (Math.Abs(diff.X) > avoidDist.X && Math.Abs(diff.Y) > avoidDist.Y)
548  {
549  debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, 0.0f, Vector2.Zero, Vector2.Zero));
550  continue;
551  }
552  if (diff.LengthSquared() < 1.0f) { diff = Vector2.UnitY; }
553 
554  Vector2 normalizedDiff = Vector2.Normalize(diff);
555  float dot = controlledSub.Velocity == Vector2.Zero ?
556  0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff);
557 
558  //not heading towards the wall -> ignore
559  if (dot < 1.0)
560  {
561  debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation));
562  continue;
563  }
564 
565  Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius;
566  if (change.LengthSquared() < 0.001f) { continue; }
567  newAvoidStrength += change * (dot - 1.0f);
568  debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation));
569  }
570  }
571  }
572 
573  avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f);
574  TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp);
575 
576  //steer away from other subs
577  foreach (Submarine sub in Submarine.Loaded)
578  {
579  if (sub == controlledSub || connectedSubs.Contains(sub)) { continue; }
580  Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size;
581  Vector2 minDist = sizeSum.ToVector2() / 2;
582  Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition;
583  float xDist = Math.Abs(diff.X);
584  float yDist = Math.Abs(diff.Y);
585  Vector2 maxAvoidDistance = minDist * 2;
586  if (xDist > maxAvoidDistance.X || yDist > maxAvoidDistance.Y)
587  {
588  //far enough -> ignore
589  continue;
590  }
591  float dot = controlledSub.Velocity == Vector2.Zero ? 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -diff);
592  if (dot < 0.0f)
593  {
594  //heading away -> ignore
595  continue;
596  }
597  float distanceFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(maxAvoidDistance.X + maxAvoidDistance.Y, minDist.X + minDist.Y, xDist + yDist));
598  float velocityFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 3, controlledSub.Velocity.Length()));
599  TargetVelocity += 100 * Vector2.Normalize(diff) * distanceFactor * velocityFactor;
600  }
601 
602  //clamp velocity magnitude to 100.0f (Is this required? The X and Y components are clamped in the property setter)
603  float velMagnitude = TargetVelocity.Length();
604  if (velMagnitude > 100.0f)
605  {
606  TargetVelocity *= 100.0f / velMagnitude;
607  }
608 
609 #if CLIENT
610  HintManager.OnAutoPilotPathUpdated(this);
611 #endif
612  }
613 
614  private float? GetNodePenalty(PathNode node, PathNode nextNode)
615  {
616  if (node.Waypoint?.Tunnel == null || controlledSub == null || node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath) { return 0.0f; }
617  //never navigate from the main path to another type of path
618  if (node.Waypoint.Tunnel.Type == Level.TunnelType.MainPath && nextNode.Waypoint?.Tunnel?.Type != Level.TunnelType.MainPath) { return null; }
619  //higher cost for side paths (= autopilot prefers the main path, but can still navigate side paths if it ends up on one)
620  return 1000.0f;
621  }
622 
623  private void UpdatePath()
624  {
625  if (Level.Loaded == null) { return; }
626 
627  if (pathFinder == null)
628  {
629  pathFinder = new PathFinder(WayPoint.WayPointList, false);
630  }
631 
632  Vector2 target;
633 
634  if (navigateTactically)
635  {
636  target = ConvertUnits.ToSimUnits(AITacticalTarget);
637  }
638  else if (LevelEndSelected)
639  {
640  target = ConvertUnits.ToSimUnits(Level.Loaded.EndExitPosition);
641  }
642  else
643  {
644  target = ConvertUnits.ToSimUnits(Level.Loaded.StartExitPosition);
645  }
646  steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(controlledSub == null ? item.WorldPosition : controlledSub.WorldPosition), target, errorMsgStr: "(Autopilot, target: " + target + ")");
647  }
648 
650  {
651  AutoPilot = true;
652  MaintainPos = false;
653  posToMaintain = null;
654  LevelEndSelected = false;
655  navigateTactically = false;
656  if (!LevelStartSelected)
657  {
658  LevelStartSelected = true;
659  UpdatePath();
660  }
661  }
662 
664  {
665  AutoPilot = true;
666  MaintainPos = false;
667  posToMaintain = null;
668  LevelStartSelected = false;
669  navigateTactically = false;
670  if (!LevelEndSelected)
671  {
672  LevelEndSelected = true;
673  UpdatePath();
674  }
675  }
676 
677  private void SetDestinationTactical()
678  {
679  AutoPilot = true;
680  MaintainPos = false;
681  posToMaintain = null;
682  LevelStartSelected = false;
683  LevelEndSelected = false;
684  if (!navigateTactically)
685  {
686  navigateTactically = true;
687  UpdatePath();
688  }
689  }
690 
691  private void SetMaintainPosition()
692  {
693  if (!MaintainPos)
694  {
695  unsentChanges = true;
696  MaintainPos = true;
697  }
698  if (!posToMaintain.HasValue)
699  {
700  unsentChanges = true;
701  posToMaintain = controlledSub != null ?
702  controlledSub.WorldPosition :
704  }
705  }
706 
713  private Vector2 GetSteeringVelocity(Vector2 worldPosition, float slowdownAmount)
714  {
715  Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * slowdownAmount;
716  Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition);
717 
718  if (targetSpeed.LengthSquared() > 500.0f * 500.0f)
719  {
720  return Vector2.Normalize(targetSpeed) * 100.0f;
721  }
722  else
723  {
724  return targetSpeed / 5.0f;
725  }
726  }
727 
728  public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
729  {
730  character.AIController.SteeringManager.Reset();
731  if (objective.Override)
732  {
733  if (user != character && user != null && user.SelectedItem == item && character.IsOnPlayerTeam)
734  {
735  character.Speak(TextManager.Get("DialogSteeringTaken").Value, null, 0.0f, "steeringtaken".ToIdentifier(), 10.0f);
736  }
737  }
738  user = character;
739 
741  {
742  if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
743  {
744  objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true));
745  return false;
746  }
747  else
748  {
749  character.Speak(TextManager.Get("DialogNavTerminalIsBroken").Value, identifier: "navterminalisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
750  }
751  }
752 
753  if (!AutoPilot)
754  {
755  unsentChanges = true;
756  AutoPilot = true;
757  }
758  IncreaseSkillLevel(user, deltaTime);
759  if (objective.Option == "maintainposition")
760  {
761  if (objective.Override)
762  {
763  SetMaintainPosition();
764  }
765  }
766  else if (!Level.IsLoadedOutpost)
767  {
768  if (objective.Option == "navigateback")
769  {
770  if (DockingSources.Any(d => d.Docked))
771  {
772  item.SendSignal("1", "toggle_docking");
773  }
774 
775  if (objective.Override)
776  {
777  if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically)
778  {
779  unsentChanges = true;
780  }
781 
783  }
784  }
785  else if (objective.Option == "navigatetodestination")
786  {
787  if (DockingSources.Any(d => d.Docked))
788  {
789  item.SendSignal("1", "toggle_docking");
790  }
791 
792  if (objective.Override)
793  {
794  if (MaintainPos || !LevelEndSelected || LevelStartSelected || navigateTactically)
795  {
796  unsentChanges = true;
797  }
798 
800  }
801  }
802  else if (objective.Option == "navigatetactical")
803  {
804  if (DockingSources.Any(d => d.Docked))
805  {
806  item.SendSignal("1", "toggle_docking");
807  }
808 
809  if (objective.Override)
810  {
811  if (MaintainPos || LevelEndSelected || LevelStartSelected || !navigateTactically)
812  {
813  unsentChanges = true;
814  }
815 
816  SetDestinationTactical();
817  }
818  }
819  }
820 
821  sonar?.CrewAIOperate(deltaTime, character, objective);
822  if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam)
823  {
824  character.Speak(TextManager.Get("dialogicespirespottedsonar").Value, null, 0.0f, "icespirespottedsonar".ToIdentifier(), 60.0f);
825  }
826  return false;
827  }
828 
829  public override void ReceiveSignal(Signal signal, Connection connection)
830  {
831  if (connection.Name == "velocity_in")
832  {
833  steeringAdjustSpeed = DefaultSteeringAdjustSpeed;
834  steeringInput = XMLExtensions.ParseVector2(signal.value, errorMessages: false);
835  steeringInput.X = MathHelper.Clamp(steeringInput.X, -100.0f, 100.0f);
836  steeringInput.Y = MathHelper.Clamp(-steeringInput.Y, -100.0f, 100.0f);
837  TargetVelocity = steeringInput;
838  lastReceivedSteeringSignalTime = Timing.TotalTime;
839  }
840  else
841  {
842  base.ReceiveSignal(signal, connection);
843  }
844  }
845  }
846 }
void AddSubObjective(AIObjective objective, bool addFirst=false)
override bool IsValidTarget(Item item)
float GetSkillLevel(string skillIdentifier)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility=false, float maxGain=2f)
Increases the characters skill at a rate proportional to their current skill. If you want to increase...
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static GameClient Client
Definition: GameMain.cs:188
void SendSignal(string signal, string connectionName)
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
const float PressureWarningThreshold
How many units before crush depth the pressure warning is shown
float GetRealWorldDepth(float worldPositionY)
Calculate the "real" depth in meters from the surface of Europa (the value you see on the nav termina...
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
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
WayPoint CheckProgress(Vector2 simPosition, float minSimDistance=0.1f)
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? Borders
Extents of the solid items/structures (ones with a physics body) and hulls
List< GraphEdge > Edges
Interface for entities that the clients can send events to the server
Interface for entities that the server can send events to the clients
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19