Client LuaCsForBarotrauma
LatchOntoAI.cs
1 using FarseerPhysics;
2 using FarseerPhysics.Dynamics;
3 using FarseerPhysics.Dynamics.Joints;
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Xml.Linq;
8 using System.Linq;
9 using Voronoi2;
10 
11 namespace Barotrauma
12 {
14  {
15  const float RaycastInterval = 5.0f;
16  private float raycastTimer;
17  private Body targetBody;
18  private Vector2 attachSurfaceNormal;
19  private readonly Character character;
20 
21  public bool AttachToSub { get; private set; }
22  public bool AttachToWalls { get; private set; }
23  public bool AttachToCharacters { get; private set; }
24 
25  public Submarine TargetSubmarine { get; private set; }
26  public Structure TargetWall { get; private set; }
27  public Character TargetCharacter { get; private set; }
28 
29  private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
30  private readonly float damageOnDetach, detachStun;
31  private readonly bool weld;
32  private float deattachCheckTimer;
33 
34  private Vector2 _attachPos;
35 
39  private float attachCooldown;
40 
41  private readonly Limb attachLimb;
42  private Vector2 localAttachPos;
43  private readonly float attachLimbRotation;
44 
45  private float jointDir;
46 
47  private float latchedDuration;
48 
49  private readonly bool freezeWhenLatched;
50 
51  public List<Joint> AttachJoints { get; } = new List<Joint>();
52 
53  public Vector2? AttachPos
54  {
55  get;
56  private set;
57  }
58 
59  public bool IsAttached => AttachJoints.Count > 0;
60 
61  public bool IsAttachedToSub => IsAttached && TargetSubmarine != null && TargetCharacter == null;
62 
63  public LatchOntoAI(XElement element, EnemyAIController enemyAI)
64  {
65  AttachToWalls = element.GetAttributeBool(nameof(AttachToWalls), false);
66  AttachToSub = element.GetAttributeBool(nameof(AttachToSub), false);
67  AttachToCharacters = element.GetAttributeBool(nameof(AttachToCharacters), false);
68  minDeattachSpeed = element.GetAttributeFloat(nameof(minDeattachSpeed), 5.0f);
69  maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat(nameof(maxDeattachSpeed), 8.0f));
70  maxAttachDuration = element.GetAttributeFloat(nameof(maxAttachDuration), -1.0f);
71  coolDown = element.GetAttributeFloat(nameof(coolDown), 2f);
72  damageOnDetach = element.GetAttributeFloat(nameof(damageOnDetach), 0.0f);
73  detachStun = element.GetAttributeFloat(nameof(detachStun), 0.0f);
74  localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2(nameof(localAttachPos), Vector2.Zero));
75  attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat(nameof(attachLimbRotation), 0.0f));
76  weld = element.GetAttributeBool(nameof(weld), true);
77  freezeWhenLatched = element.GetAttributeBool(nameof(freezeWhenLatched), false);
78 
79  string limbString = element.GetAttributeString("attachlimb", null);
80  attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase));
81  if (attachLimb == null)
82  {
83  if (Enum.TryParse(limbString, out LimbType attachLimbType))
84  {
85  attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
86  }
87  }
88  if (attachLimb == null)
89  {
90  attachLimb = enemyAI.Character.AnimController.MainLimb;
91  }
92 
93  character = enemyAI.Character;
94  enemyAI.Character.OnDeath += OnCharacterDeath;
95  }
96 
97  public void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
98  {
99  if (!AttachToSub) { return; }
100  if (wall == null) { return; }
101  var sub = wall.Submarine;
102  if (sub == null) { return; }
103  Reset();
104  TargetWall = wall;
105  TargetSubmarine = sub;
107  this.attachSurfaceNormal = attachSurfaceNormal;
108  _attachPos = attachPos;
109  }
110 
111  public void SetAttachTarget(Character target)
112  {
113  if (!AttachToCharacters) { return; }
114  if (target.Submarine != character.Submarine) { return; }
115  Reset();
116  TargetCharacter = target;
117  targetBody = target.AnimController.Collider.FarseerBody;
118  attachSurfaceNormal = Vector2.Normalize(character.WorldPosition - target.WorldPosition);
119  }
120 
121  public void SetAttachTarget(VoronoiCell levelWall)
122  {
123  if (!AttachToWalls) { return; }
124  Reset();
125  foreach (Voronoi2.GraphEdge edge in levelWall.Edges)
126  {
127  if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, levelWall.Center, out Vector2 intersection))
128  {
129  attachSurfaceNormal = edge.GetNormal(levelWall);
130  targetBody = levelWall.Body;
131  _attachPos = ConvertUnits.ToSimUnits(intersection);
132  return;
133  }
134  }
135  }
136 
137  public void Update(EnemyAIController enemyAI, float deltaTime)
138  {
139  if (TargetCharacter != null && character.Submarine != TargetCharacter.Submarine ||
140  character.Submarine != null && TargetSubmarine != null && TargetCharacter == null)
141  {
142  DeattachFromBody(reset: true);
143  return;
144  }
145  if (IsAttached)
146  {
147  latchedDuration += deltaTime;
148  if (freezeWhenLatched && targetBody is { BodyType: BodyType.Static } &&
149  /*brief delay to let the ragdoll "settle"*/
150  latchedDuration > 5.0f)
151  {
152  foreach (var limb in character.AnimController.Limbs)
153  {
154  limb.body.LinearVelocity = Vector2.Zero;
155  limb.body.AngularVelocity = 0.0f;
156  }
157  }
158  if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
159  {
160  var attachJoint = AttachJoints[0];
161  if (attachJoint is WeldJoint weldJoint)
162  {
163  weldJoint.LocalAnchorA = new Vector2(-weldJoint.LocalAnchorA.X, weldJoint.LocalAnchorA.Y);
164  weldJoint.ReferenceAngle = -weldJoint.ReferenceAngle;
165  }
166  else if (attachJoint is RevoluteJoint revoluteJoint)
167  {
168  revoluteJoint.LocalAnchorA = new Vector2(-revoluteJoint.LocalAnchorA.X, revoluteJoint.LocalAnchorA.Y);
169  revoluteJoint.ReferenceAngle = -revoluteJoint.ReferenceAngle;
170  }
171  jointDir = attachLimb.Dir;
172  }
173  for (int i = 0; i < AttachJoints.Count; i++)
174  {
175  //something went wrong, limb body is very far from the joint anchor -> deattach
176  if (Vector2.DistanceSquared(AttachJoints[i].WorldAnchorB, AttachJoints[i].BodyA.Position) > 10.0f * 10.0f)
177  {
178 #if DEBUG
179  DebugConsole.Log("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
180 #endif
181  DeattachFromBody(reset: true);
182  return;
183  }
184  }
185  if (TargetCharacter != null)
186  {
187  if (enemyAI.AttackLimb?.attack == null)
188  {
189  DeattachFromBody(reset: true, cooldown: 1);
190  }
191  else
192  {
193  float range = enemyAI.AttackLimb.attack.DamageRange * 2f;
194  if (Vector2.DistanceSquared(TargetCharacter.WorldPosition, enemyAI.AttackLimb.WorldPosition) > range * range)
195  {
196  DeattachFromBody(reset: true, cooldown: 1);
197  }
198  else
199  {
200  TargetCharacter.Latchers.Add(this);
201  }
202  }
203  }
204  }
205 
206  if (attachCooldown > 0)
207  {
208  attachCooldown -= deltaTime;
209  }
210  if (deattachCheckTimer > 0)
211  {
212  deattachCheckTimer -= deltaTime;
213  }
214 
215  if (TargetCharacter != null)
216  {
217  // Own sim pos -> target where we are
218  _attachPos = character.SimPosition;
219  }
220  Vector2 transformedAttachPos = _attachPos;
221  if (character.Submarine == null && TargetSubmarine != null)
222  {
223  transformedAttachPos += ConvertUnits.ToSimUnits(TargetSubmarine.Position);
224  }
225  if (transformedAttachPos != Vector2.Zero)
226  {
227  AttachPos = transformedAttachPos;
228  }
229 
230  switch (enemyAI.State)
231  {
232  case AIState.Idle:
233  if (AttachToWalls && character.Submarine == null && Level.Loaded != null)
234  {
235  if (!IsAttached)
236  {
237  raycastTimer -= deltaTime;
238  //check if there are any walls nearby the character could attach to
239  if (raycastTimer < 0.0f)
240  {
241  _attachPos = Vector2.Zero;
242 
243  var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
244  if (cells.Count > 0)
245  {
246  //ignore walls more than 200 meters away
247  float closestDist = 200.0f * 200.0f;
248  foreach (Voronoi2.VoronoiCell cell in cells)
249  {
250  foreach (Voronoi2.GraphEdge edge in cell.Edges)
251  {
252  if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
253  {
254  Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
255  float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
256  if (distSqr < closestDist)
257  {
258  attachSurfaceNormal = edge.GetNormal(cell);
259  targetBody = cell.Body;
260  _attachPos = potentialAttachPos;
261  closestDist = distSqr;
262  }
263  break;
264  }
265  }
266  }
267  }
268  raycastTimer = RaycastInterval;
269  }
270  }
271  }
272  else
273  {
274  _attachPos = Vector2.Zero;
275  }
276  if (_attachPos == Vector2.Zero || targetBody == null)
277  {
278  DeattachFromBody(reset: false);
279  }
280  else if (attachCooldown <= 0.0f)
281  {
282  float squaredDistance = Vector2.DistanceSquared(character.SimPosition, _attachPos);
283  float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.Radius, character.AnimController.Collider.Width), character.AnimController.Collider.Height) * 1.2f;
284  if (squaredDistance < targetDistance * targetDistance)
285  {
286  //close enough to a wall -> attach
287  AttachToBody(_attachPos);
288  enemyAI.SteeringManager.Reset();
289  }
290  else
291  {
292  //move closer to the wall
293  DeattachFromBody(reset: false);
294  enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, 0.1f);
295  enemyAI.SteeringManager.SteeringSeek(_attachPos);
296  }
297  }
298  else if (IsAttached)
299  {
300  enemyAI.SteeringManager.Reset();
301  }
302  break;
303  case AIState.Attack:
304  case AIState.Aggressive:
305  if (enemyAI.IsSteeringThroughGap) { break; }
306  if (_attachPos == Vector2.Zero) { break; }
307  if (!AttachToSub && !AttachToCharacters) { break; }
308  if (enemyAI.AttackLimb == null) { break; }
309  if (targetBody == null) { break; }
310  if (IsAttached && AttachJoints[0].BodyB == targetBody) { break; }
311  Vector2 referencePos = TargetCharacter?.WorldPosition ?? ConvertUnits.ToDisplayUnits(transformedAttachPos);
312  if (Vector2.DistanceSquared(referencePos, enemyAI.AttackLimb.WorldPosition) < enemyAI.AttackLimb.attack.DamageRange * enemyAI.AttackLimb.attack.DamageRange)
313  {
314  AttachToBody(transformedAttachPos);
315  }
316  break;
317  default:
318  DeattachFromBody(reset: true);
319  break;
320  }
321 
322  if (IsAttached && targetBody != null && deattachCheckTimer <= 0.0f)
323  {
324  attachCooldown = coolDown;
325  bool deattach = false;
326  if (maxAttachDuration > 0)
327  {
328  deattach = true;
329  }
330  if (!deattach && TargetWall != null && TargetSubmarine != null)
331  {
332  // Deattach if the wall is broken enough where we are attached to
333  int targetSection = TargetWall.FindSectionIndex(attachLimb.WorldPosition, world: true, clamp: true);
334  if (enemyAI.CanPassThroughHole(TargetWall, targetSection))
335  {
336  deattach = true;
337  }
338  if (!deattach)
339  {
340  // Deattach if the velocity is high
341  float velocity = TargetSubmarine.Velocity == Vector2.Zero ? 0.0f : TargetSubmarine.Velocity.Length();
342  deattach = velocity > maxDeattachSpeed;
343  if (!deattach)
344  {
345  if (velocity > minDeattachSpeed)
346  {
347  float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
348  Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
349  (Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
350 
351  if (Rand.Range(0.0f, 1.0f) < velocityFactor)
352  {
353  deattach = true;
354  character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
355  attachCooldown = Math.Max(detachStun * 2, coolDown);
356  }
357  }
358  }
359  }
360  deattachCheckTimer = 5.0f;
361  }
362  if (deattach)
363  {
364  DeattachFromBody(reset: true);
365  }
366  }
367  }
368 
369  public void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal = null, Vector2? forceColliderSimPosition = null)
370  {
371  if (attachLimb == null) { return; }
372  if (targetBody == null) { return; }
373  if (attachCooldown > 0) { return; }
374  var collider = character.AnimController.Collider;
375  //already attached to something
376  if (AttachJoints.Count > 0)
377  {
378  //already attached to the target body, no need to do anything
379  if (AttachJoints[0].BodyB == targetBody) { return; }
380  DeattachFromBody(reset: false);
381  }
382 
383  jointDir = attachLimb.Dir;
384 
385  if (forceAttachSurfaceNormal.HasValue) { attachSurfaceNormal = forceAttachSurfaceNormal.Value; }
386  if (forceColliderSimPosition.HasValue)
387  {
388  character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
389  }
390 
391  // TODO: Shouldn't multiply by LimbScale here, because it's already applied in attachLimb.Scale!
392  Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
393  if (jointDir < 0.0f)
394  {
395  transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
396  }
397 
398  float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
399  //make sure the angle "has the same number of revolutions" as the reference limb
400  //(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
401  angle = attachLimb.body.WrapAngleToSameNumberOfRevolutions(angle);
402  attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
403 
404  var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
405  transformedLocalAttachPos, targetBody.GetLocalPoint(attachPos), false)
406  {
407  FrequencyHz = 10.0f,
408  DampingRatio = 0.5f,
409  KinematicBodyB = true,
410  CollideConnected = false,
411  };
412  GameMain.World.Add(limbJoint);
413  AttachJoints.Add(limbJoint);
414 
415  // Limb scale is already taken into account when creating the collider.
416  Vector2 colliderFront = collider.GetLocalFront();
417  if (jointDir < 0.0f)
418  {
419  colliderFront.X = -colliderFront.X;
420  }
421  collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
422 
423  Joint colliderJoint = weld ?
424  new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
425  {
426  FrequencyHz = 10.0f,
427  DampingRatio = 0.5f,
428  KinematicBodyB = true,
429  CollideConnected = false,
430  } :
431  new RevoluteJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
432  {
433  MotorEnabled = true,
434  MaxMotorTorque = 0.25f
435  } as Joint;
436 
437  GameMain.World.Add(colliderJoint);
438  AttachJoints.Add(colliderJoint);
439  TargetCharacter?.Latchers.Add(this);
440  if (maxAttachDuration > 0)
441  {
442  deattachCheckTimer = maxAttachDuration;
443  }
444 
445 #if SERVER
446  if (TargetCharacter != null)
447  {
448  GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetCharacter, attachSurfaceNormal, attachPos));
449  }
450  else if (TargetWall != null)
451  {
452  GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, TargetWall, attachSurfaceNormal, attachPos));
453  }
454  else if (targetBody.UserData is Voronoi2.VoronoiCell cell)
455  {
456  GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData(character, cell, attachSurfaceNormal, attachPos));
457  }
458 #endif
459  }
460 
461  public void DeattachFromBody(bool reset, float cooldown = 0)
462  {
463  bool wasAttached = IsAttached;
464  foreach (Joint joint in AttachJoints)
465  {
466  GameMain.World.Remove(joint);
467  }
468  AttachJoints.Clear();
469  if (cooldown > 0)
470  {
471  attachCooldown = cooldown;
472  }
473  TargetCharacter?.Latchers.Remove(this);
474  if (reset)
475  {
476  Reset();
477  }
478 #if SERVER
479  if (wasAttached)
480  {
481  GameMain.Server.CreateEntityEvent(character, new Character.LatchedOntoTargetEventData());
482  }
483 #endif
484  }
485 
486  private void Reset()
487  {
488  TargetCharacter?.Latchers.Remove(this);
489  TargetCharacter = null;
490  TargetWall = null;
491  TargetSubmarine = null;
492  targetBody = null;
493  AttachPos = null;
494  }
495 
496  private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
497  {
498  DeattachFromBody(reset: true);
499  character.OnDeath -= OnCharacterDeath;
500  }
501  }
502 }
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
Affliction Instantiate(float strength, Character source=null)
static AfflictionPrefab InternalDamage
bool CanPassThroughHole(Structure wall, int sectionIndex)
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static World World
Definition: GameMain.cs:105
void SetAttachTarget(Structure wall, Vector2 attachPos, Vector2 attachSurfaceNormal)
Definition: LatchOntoAI.cs:97
void DeattachFromBody(bool reset, float cooldown=0)
Definition: LatchOntoAI.cs:461
void AttachToBody(Vector2 attachPos, Vector2? forceAttachSurfaceNormal=null, Vector2? forceColliderSimPosition=null)
Definition: LatchOntoAI.cs:369
void SetAttachTarget(VoronoiCell levelWall)
Definition: LatchOntoAI.cs:121
List< Joint > AttachJoints
Definition: LatchOntoAI.cs:51
Submarine TargetSubmarine
Definition: LatchOntoAI.cs:25
void SetAttachTarget(Character target)
Definition: LatchOntoAI.cs:111
LatchOntoAI(XElement element, EnemyAIController enemyAI)
Definition: LatchOntoAI.cs:63
void Update(EnemyAIController enemyAI, float deltaTime)
Definition: LatchOntoAI.cs:137
Character TargetCharacter
Definition: LatchOntoAI.cs:27
List< VoronoiCell > GetCells(Vector2 worldPos, int searchDepth=2)
Limb GetLimb(LimbType limbType, bool excludeSevered=true, bool excludeLimbsWithSecondaryType=false, bool useSecondaryType=false)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
void SteeringSeek(Vector2 targetSimPos, float weight=1)
void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight=1)
int FindSectionIndex(Vector2 displayPos, bool world=false, bool clamp=false)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
List< GraphEdge > Edges