Server LuaCsForBarotrauma
Turret.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;
9 using FarseerPhysics.Dynamics;
10 using System.Collections.Immutable;
11 
13 {
15  {
16  private Sprite barrelSprite, railSprite;
17  private Sprite barrelSpriteBroken, railSpriteBroken;
18  private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>();
19  private readonly List<Sprite> spinningBarrelSprites = new List<Sprite>();
20 
24  const ushort LaunchWithoutProjectileId = ushort.MaxValue;
25 
26  private Vector2 barrelPos;
27  private Vector2 transformedBarrelPos;
28 
29  private float targetRotation;
30 
31  private float reload;
32  private int shotCounter;
33 
34  private float minRotation, maxRotation;
35 
36  private Camera cam;
37 
38  private float angularVelocity;
39 
40  private int failedLaunchAttempts;
41 
42  private float currentChargeTime;
43  private bool tryingToCharge;
44 
45  private enum ChargingState
46  {
47  Inactive,
48  WindingUp,
49  WindingDown,
50  }
51 
52  private ChargingState currentChargingState;
53 
54  private readonly List<Item> activeProjectiles = new List<Item>();
55  public IEnumerable<Item> ActiveProjectiles => activeProjectiles;
56 
57  private Character user;
58 
59  private float resetUserTimer;
60 
61  private float aiFindTargetTimer;
62  private ISpatialEntity currentTarget;
63  private const float CrewAiFindTargetMaxInterval = 1.0f;
64  private const float CrewAIFindTargetMinInverval = 0.2f;
65 
71  private const float MinimumProjectileVelocityForAimAhead = 20.0f;
72 
77  private const float MaximumAimAhead = 10.0f;
78 
79  private float projectileSpeed;
80  private Item previousAmmo;
81 
82  private int currentLoaderIndex;
83 
84  private const float TinkeringPowerCostReduction = 0.2f;
85  private const float TinkeringDamageIncrease = 0.2f;
86  private const float TinkeringReloadDecrease = 0.2f;
87 
89  private float resetActiveUserTimer;
90 
91  private List<LightComponent> lightComponents;
92 
93  private Projectile lastProjectile;
94 
95  private readonly bool isSlowTurret;
96 
97  public float Rotation { get; private set; }
98 
99  [Serialize("0,0", IsPropertySaveable.No, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")]
100  public Vector2 BarrelPos
101  {
102  get
103  {
104  return barrelPos;
105  }
106  set
107  {
108  barrelPos = value;
109  UpdateTransformedBarrelPos();
110  }
111  }
112 
113  [Serialize("0,0", IsPropertySaveable.No, description: "The projectile launching location relative to transformed barrel position (in pixels).")]
114  public Vector2 FiringOffset { get; set; }
115 
116  private bool flipFiringOffset;
117 
118  [Serialize(false, IsPropertySaveable.No, description: "If enabled, the firing offset will alternate from left to right (i.e. flipping the x-component of the offset each shot.)")]
119  public bool AlternatingFiringOffset { get; set; }
120 
121  public Vector2 TransformedBarrelPos => transformedBarrelPos;
122 
123  [Serialize(0.0f, IsPropertySaveable.No, description: "The impulse applied to the physics body of the projectile (the higher the impulse, the faster the projectiles are launched).")]
124  public float LaunchImpulse { get; set; }
125 
126  [Serialize(1.0f, IsPropertySaveable.No, description: "Multiplies the damage the turret deals by this amount.")]
127  public float DamageMultiplier { get; set; }
128 
129  [Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
130  public int ProjectileCount { get; set; }
131 
132  [Serialize(false, IsPropertySaveable.No, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")]
133  public bool LaunchWithoutProjectile { get; set; }
134 
135  [Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles (in degrees).")]
136  public float Spread { get; set; }
137 
138  [Serialize(1.0f, IsPropertySaveable.No, description: "How fast the turret can rotate while firing (for charged weapons).")]
139  public float FiringRotationSpeedModifier { get; set; }
140 
141  [Serialize(false, IsPropertySaveable.Yes, description: "Whether the turret should always charge-up fully to shoot.")]
142  public bool SingleChargedShot { get; set; }
143 
144  private float prevScale;
145  float prevBaseRotation;
146  [Serialize(0.0f, IsPropertySaveable.Yes, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
147  public float BaseRotation
148  {
149  get { return item.Rotation; }
150  set
151  {
152  item.Rotation = value;
153  UpdateTransformedBarrelPos();
154  }
155  }
156 
157  [Serialize(3500.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
158  public float AIRange { get; set; }
159 
160  private float _maxAngleOffset;
161  [Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")]
162  public float MaxAngleOffset
163  {
164  get => _maxAngleOffset;
165  private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f);
166  }
167 
168  [Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")]
169  public float AICurrentTargetPriorityMultiplier { get; private set; }
170 
171  [Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")]
172  public int MaxActiveProjectiles { get; set; }
173 
174  [Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
175  public float MaxChargeTime { get; private set; }
176 
177  #region Editable properties
178 
179  [Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots."),
180  Editable(0.0f, 1000.0f, decimals: 3)]
181  public float Reload { get; set; }
182 
183  [Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun."),
184  Editable(1, 100)]
185  public int ShotsPerBurst { get; set; }
186 
187  [Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload."),
188  Editable(0.0f, 1000.0f, decimals: 3)]
189  public float DelayBetweenBursts { get; set; }
190 
191  [Serialize(1.0f, IsPropertySaveable.No, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too."),
192  Editable(0.1f, 10f)]
193  public float RetractionDurationMultiplier { get; set; }
194 
195  [Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching."),
196  Editable(0.1f, 10f)]
197  public float RecoilTime { get; set; }
198 
199  [Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position."),
200  Editable(0f, 1000f)]
201  public float RetractionDelay { get; set; }
202 
203  [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }),
204  Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)]
205  public Vector2 RotationLimits
206  {
207  get
208  {
209  return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation));
210  }
211  set
212  {
213  float newMinRotation = MathHelper.ToRadians(value.X);
214  float newMaxRotation = MathHelper.ToRadians(value.Y);
215 
216  bool minRotationModified = MathHelper.Distance(newMinRotation, minRotation) > 0.02f;
217  bool maxRotationModified = MathHelper.Distance(newMaxRotation, maxRotation) > 0.02f;
218 
219  // if only one rotation changes (when editing via text field), use the other one to clamp to max range
220  if (minRotationModified && !maxRotationModified)
221  {
222  newMinRotation = MathHelper.Clamp(newMinRotation, maxRotation - MathHelper.TwoPi, maxRotation);
223  }
224  else if (!minRotationModified && maxRotationModified)
225  {
226  newMaxRotation = MathHelper.Clamp(newMaxRotation, minRotation, minRotation + MathHelper.TwoPi);
227  }
228 
229  maxRotation = newMaxRotation;
230  minRotation = newMinRotation;
231 
232  Rotation = (minRotation + maxRotation) / 2;
233 #if CLIENT
234  if (lightComponents != null)
235  {
236  foreach (var light in lightComponents)
237  {
238  light.Rotation = Rotation;
239  light.Light.Rotation = -Rotation;
240  }
241  }
242 #endif
243  }
244  }
245 
246  [Serialize(5.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character with insufficient skills to operate it. Higher values make the barrel rotate faster."),
247  Editable(0.0f, 1000.0f, DecimalCount = 2)]
248  public float SpringStiffnessLowSkill { get; private set; }
249 
250  [Serialize(2.0f, IsPropertySaveable.No, description: "How much torque is applied to rotate the barrel when the item is used by a character with sufficient skills to operate it. Higher values make the barrel rotate faster."),
251  Editable(0.0f, 1000.0f, DecimalCount = 2)]
252  public float SpringStiffnessHighSkill { get; private set; }
253 
254  [Serialize(50.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character with insufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at."),
255  Editable(0.0f, 1000.0f, DecimalCount = 2)]
256  public float SpringDampingLowSkill { get; private set; }
257 
258  [Serialize(10.0f, IsPropertySaveable.No, description: "How much torque is applied to resist the movement of the barrel when the item is used by a character with sufficient skills to operate it. Higher values make the aiming more \"snappy\", stopping the barrel from swinging around the direction it's being aimed at."),
259  Editable(0.0f, 1000.0f, DecimalCount = 2)]
260  public float SpringDampingHighSkill { get; private set; }
261 
262  [Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it."),
263  Editable(0.0f, 100.0f, DecimalCount = 2)]
264  public float RotationSpeedLowSkill { get; private set; }
265 
266  [Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),
267  Editable(0.0f, 100.0f, DecimalCount = 2)]
268  public float RotationSpeedHighSkill { get; private set; }
269 
270  [Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A)."),
271  Editable]
272  public Color HudTint { get; set; }
273 
274  [Header(localizedTextTag: "sp.turret.AutoOperate.propertyheader")]
275  [Serialize(false, IsPropertySaveable.Yes, description: "Should the turret operate automatically using AI targeting? Comes with some optional random movement that can be adjusted below."),
276  Editable(TransferToSwappedItem = true)]
277  public bool AutoOperate { get; set; }
278 
279  [Serialize(false, IsPropertySaveable.Yes, description: "Can the Auto Operate functionality be enabled using signals to the turret?"),
280  Editable(TransferToSwappedItem = true)]
281  public bool AllowAutoOperateWithWiring { get; set; }
282 
283  [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How much the turret should adjust the aim off the target randomly instead of tracking the target perfectly? In Degrees."),
284  Editable(TransferToSwappedItem = true)]
285  public float RandomAimAmount { get; set; }
286 
287  [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Minimum wait time, in seconds."),
288  Editable(TransferToSwappedItem = true)]
289  public float RandomAimMinTime { get; set; }
290 
291  [Serialize(0f, IsPropertySaveable.Yes, description: "[Auto Operate] How often the turret should adjust the aim randomly instead of tracking the target perfectly? Maximum wait time, in seconds."),
292  Editable(TransferToSwappedItem = true)]
293  public float RandomAimMaxTime { get; set; }
294 
295  [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret move randomly while idle?"),
296  Editable(TransferToSwappedItem = true)]
297  public bool RandomMovement { get; set; }
298 
299  [Serialize(false, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret have a delay while targeting targets or always aim prefectly?"),
300  Editable(TransferToSwappedItem = true)]
301  public bool AimDelay { get; set; }
302 
303  [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target characters in general?"),
304  Editable(TransferToSwappedItem = true)]
305  public bool TargetCharacters { get; set; }
306 
307  [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all monsters?"),
308  Editable(TransferToSwappedItem = true)]
309  public bool TargetMonsters { get; set; }
310 
311  [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target all humans (or creatures in the same group, like pets)?"),
312  Editable(TransferToSwappedItem = true)]
313  public bool TargetHumans { get; set; }
314 
315  [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target other submarines?"),
316  Editable(TransferToSwappedItem = true)]
317  public bool TargetSubmarines { get; set; }
318 
319  [Serialize(true, IsPropertySaveable.Yes, description: "[Auto Operate] Should the turret target items?"),
320  Editable(TransferToSwappedItem = true)]
321  public bool TargetItems { get; set; }
322 
323  [Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."),
324  Editable(TransferToSwappedItem = true)]
325  public Identifier FriendlyTag { get; private set; }
326 
327  [Serialize("None", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly. If set to None, the team the submarine/outpost belongs to is considered the friendly team."),
328  Editable(TransferToSwappedItem = true)]
329  public CharacterTeamType FriendlyTeam { get; private set; }
330  #endregion
331 
332  private const string SetAutoOperateConnection = "set_auto_operate";
333  private const string ToggleAutoOperateConnection = "toggle_auto_operate";
334 
335  public Turret(Item item, ContentXElement element)
336  : base(item, element)
337  {
338  IsActive = true;
339 
340  foreach (var subElement in element.Elements())
341  {
342  switch (subElement.Name.ToString().ToLowerInvariant())
343  {
344  case "barrelsprite":
345  barrelSprite = new Sprite(subElement);
346  break;
347  case "railsprite":
348  railSprite = new Sprite(subElement);
349  break;
350  case "barrelspritebroken":
351  barrelSpriteBroken = new Sprite(subElement);
352  break;
353  case "railspritebroken":
354  railSpriteBroken = new Sprite(subElement);
355  break;
356  case "chargesprite":
357  chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero)));
358  break;
359  case "spinningbarrelsprite":
360  int spriteCount = subElement.GetAttributeInt("spriteamount", 1);
361  for (int i = 0; i < spriteCount; i++)
362  {
363  spinningBarrelSprites.Add(new Sprite(subElement));
364  }
365  break;
366  }
367  }
368  item.IsShootable = true;
369  item.RequireAimToUse = false;
370  isSlowTurret = item.HasTag("slowturret".ToIdentifier());
371  InitProjSpecific(element);
372  }
373 
374  partial void InitProjSpecific(ContentXElement element);
375 
376  private void UpdateTransformedBarrelPos()
377  {
378  transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), MathHelper.ToRadians(item.Rotation));
379 #if CLIENT
380  item.ResetCachedVisibleSize();
381 #endif
382  prevBaseRotation = item.Rotation;
383  prevScale = item.Scale;
384  }
385 
386  public override void OnMapLoaded()
387  {
388  base.OnMapLoaded();
389  if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
390  if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
391  targetRotation = Rotation;
392  UpdateTransformedBarrelPos();
394  Screen.Selected is { IsEditor: false })
395  {
396  // If the turret is not set to auto operate and the auto operate connections haven't been wired to anything,
397  // don't allow changing the state with wirings.
398  foreach (ConnectionPanel connectionPanel in Item.GetComponents<ConnectionPanel>())
399  {
400  connectionPanel.Connections.RemoveAll(c => c.Name is ToggleAutoOperateConnection or SetAutoOperateConnection && c.Wires.None());
401  }
402  }
403  }
404 
405  private void FindLightComponents()
406  {
407  if (lightComponents != null)
408  {
409  // Can't run again, because of reparenting.
410  return;
411  }
412  foreach (LightComponent lc in item.GetComponents<LightComponent>())
413  {
414  // Only make the Turret control the LightComponents that are it's children. So it'd be possible to for example have some extra lights on the turret that don't rotate with it.
415  if (lc?.Parent == this)
416  {
417  lightComponents ??= new List<LightComponent>();
418  lightComponents.Add(lc);
419  }
420  }
421 
422 #if CLIENT
423  if (lightComponents != null)
424  {
425  foreach (var light in lightComponents)
426  {
427  // We want the turret to control the state of the LightComponent, not tie it's state to the state of the Turret (the light can be inactive even if the turret is active)
428  light.Parent = null;
429  light.Rotation = Rotation - item.RotationRad;
430  light.Light.Rotation = -Rotation;
431  //turret lights are high-prio (don't want the lights to disappear when you're fighting something)
432  light.Light.PriorityMultiplier *= 10.0f;
433  }
434  }
435 #endif
436  }
437 
438  public override void Update(float deltaTime, Camera cam)
439  {
440  this.cam = cam;
441 
442  if (reload > 0.0f) { reload -= deltaTime; }
443  if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale))
444  {
445  UpdateTransformedBarrelPos();
446  }
447 
448  if (user is { Removed: true })
449  {
450  user = null;
451  }
452  else
453  {
454  resetUserTimer -= deltaTime;
455  if (resetUserTimer <= 0.0f) { user = null; }
456  }
457 
458  if (ActiveUser is { Removed: true })
459  {
460  ActiveUser = null;
461  }
462  else
463  {
464  resetActiveUserTimer -= deltaTime;
465  if (resetActiveUserTimer <= 0.0f)
466  {
467  ActiveUser = null;
468  }
469  }
470 
471  ApplyStatusEffects(ActionType.OnActive, deltaTime);
472 
473  float previousChargeTime = currentChargeTime;
474 
475  if (SingleChargedShot && reload > 0f)
476  {
477  // single charged shot guns will decharge after firing
478  // for cosmetic reasons, this is done by lerping in half the reload time
479  currentChargeTime = Reload > 0.0f ?
480  Math.Max(0f, MaxChargeTime * (reload / Reload - 0.5f)) :
481  0.0f;
482  }
483  else
484  {
485  float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime;
486  if (chargeDeltaTime > 0f && user != null)
487  {
488  chargeDeltaTime *= 1f + user.GetStatValue(StatTypes.TurretChargeSpeed);
489  }
490  currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
491  }
492  tryingToCharge = false;
493 
494  if (currentChargeTime == 0f)
495  {
496  currentChargingState = ChargingState.Inactive;
497  }
498  else if (currentChargeTime < previousChargeTime)
499  {
500  currentChargingState = ChargingState.WindingDown;
501  }
502  else
503  {
504  // if we are charging up or at maxed charge, remain winding up
505  currentChargingState = ChargingState.WindingUp;
506  }
507 
508  UpdateProjSpecific(deltaTime);
509 
510  if (MathUtils.NearlyEqual(minRotation, maxRotation))
511  {
513  return;
514  }
515 
516  float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
517 
518  float maxDist = (maxRotation - minRotation) / 2.0f;
519 
520  if (Math.Abs(targetMidDiff) > maxDist)
521  {
522  targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation;
523  }
524 
525  float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user);
526  if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels
527  float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess);
528  float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess);
529  float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess);
530  if (MaxChargeTime > 0)
531  {
532  rotationSpeed *= MathHelper.Lerp(1f, FiringRotationSpeedModifier, MathUtils.EaseIn(currentChargeTime / MaxChargeTime));
533  }
534 
535  // Do not increase the weapons skill when operating a turret in an outpost level
536  if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
537  {
538  user.Info.ApplySkillGain(
539  Tags.WeaponsSkill,
541  }
542 
543  float rotMidDiff = MathHelper.WrapAngle(Rotation - (minRotation + maxRotation) / 2.0f);
544 
545  float targetRotationDiff = MathHelper.WrapAngle(targetRotation - Rotation);
546 
547  if ((maxRotation - minRotation) < MathHelper.TwoPi)
548  {
549  float targetRotationMaxDiff = MathHelper.WrapAngle(targetRotation - maxRotation);
550  float targetRotationMinDiff = MathHelper.WrapAngle(targetRotation - minRotation);
551 
552  if (Math.Abs(targetRotationMaxDiff) < Math.Abs(targetRotationMinDiff) &&
553  rotMidDiff < 0.0f &&
554  targetRotationDiff < 0.0f)
555  {
556  targetRotationDiff += MathHelper.TwoPi;
557  }
558  else if (Math.Abs(targetRotationMaxDiff) > Math.Abs(targetRotationMinDiff) &&
559  rotMidDiff > 0.0f &&
560  targetRotationDiff > 0.0f)
561  {
562  targetRotationDiff -= MathHelper.TwoPi;
563  }
564  }
565 
566  angularVelocity +=
567  (targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime;
568  angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
569 
570  Rotation += angularVelocity * deltaTime;
571 
572  rotMidDiff = MathHelper.WrapAngle(Rotation - (minRotation + maxRotation) / 2.0f);
573 
574  if (rotMidDiff < -maxDist)
575  {
576  Rotation = minRotation;
577  angularVelocity *= -0.5f;
578  }
579  else if (rotMidDiff > maxDist)
580  {
581  Rotation = maxRotation;
582  angularVelocity *= -0.5f;
583  }
584 
585  if (aiFindTargetTimer > 0.0f)
586  {
587  aiFindTargetTimer -= deltaTime;
588  }
589 
591 
592  if (AutoOperate && ActiveUser == null)
593  {
594  UpdateAutoOperate(deltaTime, ignorePower: false);
595  }
596  }
597 
598  public void UpdateLightComponents()
599  {
600  if (lightComponents != null)
601  {
602  foreach (var light in lightComponents)
603  {
604  light.Rotation = Rotation - item.RotationRad;
605  }
606  }
607  }
608 
609  partial void UpdateProjSpecific(float deltaTime);
610 
611  private bool isUseBeingCalled;
612 
613  public override bool Use(float deltaTime, Character character = null)
614  {
615  if (!characterUsable && character != null) { return false; }
616  //prevent an infinite loop if launching triggers a StatusEffect that Uses this item
617  if (isUseBeingCalled) { return false; }
618 
619  isUseBeingCalled = true;
620  bool wasSuccessful = TryLaunch(deltaTime, character);
621  isUseBeingCalled = false;
622  return wasSuccessful;
623  }
624 
625  public float GetPowerRequiredToShoot()
626  {
627  float powerCost = powerConsumption;
628  if (user != null)
629  {
630  powerCost /= (1 + user.GetStatValue(StatTypes.TurretPowerCostReduction));
631  }
632  return powerCost;
633  }
634 
635  public bool HasPowerToShoot()
636  {
638  }
639 
640  private Vector2 GetBarrelDir()
641  {
642  return new Vector2((float)Math.Cos(Rotation), -(float)Math.Sin(Rotation));
643  }
644 
645  private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
646  {
647  tryingToCharge = true;
648  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
649 
650  if (currentChargeTime < MaxChargeTime) { return false; }
651 
652  if (reload > 0.0f) { return false; }
653 
654  if (MaxActiveProjectiles >= 0)
655  {
656  activeProjectiles.RemoveAll(it => it.Removed);
657  if (activeProjectiles.Count >= MaxActiveProjectiles)
658  {
659  return false;
660  }
661  }
662 
663  if (!ignorePower)
664  {
665  if (!HasPowerToShoot())
666  {
667 #if CLIENT
668  if (!flashLowPower && character != null && character == Character.Controlled)
669  {
670  flashLowPower = true;
671  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
672  }
673 #endif
674  return false;
675  }
676  }
677 
678  Projectile launchedProjectile = null;
679  bool loaderBroken = false;
680  float tinkeringStrength = 0f;
681 
682  for (int i = 0; i < ProjectileCount; i++)
683  {
684  var projectiles = GetLoadedProjectiles();
685  if (projectiles.Any())
686  {
687  ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
688  if (projectileContainer != null && projectileContainer.Item != item)
689  {
690  //user needs to be null because the ammo boxes shouldn't be directly usable by characters
691  projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user);
692  }
693  }
694  else
695  {
696  for (int j = 0; j < item.linkedTo.Count; j++)
697  {
698  var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
699  //use linked projectile containers in case they have to react to the turret being launched somehow
700  //(play a sound, spawn more projectiles)
701  if (e is not Item linkedItem) { continue; }
702  if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
703  if (linkedItem.Condition <= 0.0f)
704  {
705  loaderBroken = true;
706  continue;
707  }
708  if (tryUseProjectileContainer(linkedItem)) { break; }
709  }
710  tryUseProjectileContainer(item);
711 
712  bool tryUseProjectileContainer(Item containerItem)
713  {
714  ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
715  if (projectileContainer != null)
716  {
717  containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user);
718  projectiles = GetLoadedProjectiles();
719  if (projectiles.Any()) { return true; }
720  }
721  return false;
722  }
723  }
724  if (projectiles.Count == 0 && !LaunchWithoutProjectile)
725  {
726  //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched,
727  //causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay)
728  // -> attempt to launch the gun multiple times before showing the "no ammo" flash
729  failedLaunchAttempts++;
730 #if CLIENT
731  if (!flashNoAmmo && !flashLoaderBroken && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
732  {
733  if (loaderBroken)
734  {
735  flashLoaderBroken = true;
736  }
737  else
738  {
739  flashNoAmmo = true;
740  }
741  failedLaunchAttempts = 0;
742  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
743  }
744 #endif
745  return false;
746  }
747  failedLaunchAttempts = 0;
748 
749  foreach (MapEntity e in item.linkedTo)
750  {
751  if (e is not Item linkedItem) { continue; }
752  if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
753  if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag(Tags.TurretAmmoSource))
754  {
755  tinkeringStrength = repairable.TinkeringStrength;
756  }
757  }
758 
759  if (!ignorePower)
760  {
761  var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f);
762  float neededPower = GetPowerRequiredToShoot();
763  // tinkering is currently not factored into the common method as it is checked only when shooting
764  // but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later
765  neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
766  while (neededPower > 0.0001f && batteries.Any())
767  {
768  float takePower = neededPower / batteries.Count();
769  takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
770  foreach (PowerContainer battery in batteries)
771  {
772  neededPower -= takePower;
773  battery.Charge -= takePower / 3600.0f;
774 #if SERVER
775  battery.Item.CreateServerEvent(battery);
776 #endif
777  }
778  }
779  }
780 
781  launchedProjectile = projectiles.FirstOrDefault();
782  Item container = launchedProjectile?.Item.Container;
783  if (container != null)
784  {
785  var repairable = launchedProjectile?.Item.Container.GetComponent<Repairable>();
786  if (repairable != null)
787  {
788  repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
789  }
790  }
791 
792  if (launchedProjectile != null || LaunchWithoutProjectile)
793  {
794  if (launchedProjectile?.Item.GetComponent<Rope>() != null &&
795  lastProjectile?.Item.GetComponent<Rope>() is { SnapWhenWeaponFiredAgain: true } rope)
796  {
797  rope.Snap();
798  }
799 
800  if (projectiles.Any())
801  {
802  foreach (Projectile projectile in projectiles)
803  {
804  Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength);
805  }
806  }
807  else
808  {
809  Launch(null, character, tinkeringStrength: tinkeringStrength);
810  }
811  if (item.AiTarget != null)
812  {
814  // Turrets also have a light component, which handles the sight range.
815  }
816  if (container != null)
817  {
818  ShiftItemsInProjectileContainer(container.GetComponent<ItemContainer>());
819  }
820  if (item.linkedTo.Count > 0)
821  {
822  currentLoaderIndex = (currentLoaderIndex + 1) % item.linkedTo.Count;
823  }
824  }
825  }
826 
827  lastProjectile = launchedProjectile;
828 
829 #if SERVER
830  if (character != null && launchedProjectile != null)
831  {
832  string msg = GameServer.CharacterLogName(character) + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name;
833  var containedItems = launchedProjectile.Item.ContainedItems;
834  if (containedItems == null || !containedItems.Any())
835  {
836  msg += ")";
837  }
838  else
839  {
840  msg += ", contained items: " + string.Join(", ", containedItems.Select(i => i.Name)) + ")";
841  }
842  GameServer.Log(msg, ServerLog.MessageType.ItemInteraction);
843  }
844 #endif
845 
846  return true;
847  }
848 
849  private readonly struct EventData : IEventData
850  {
851  public readonly Item Projectile;
852 
853  public EventData(Item projectile, Turret turret)
854  {
855  Projectile = projectile;
856  }
857  }
858 
859  private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f)
860  {
861  reload = Reload;
862  if (ShotsPerBurst > 1)
863  {
864  shotCounter++;
865  if (shotCounter >= ShotsPerBurst)
866  {
867  reload += DelayBetweenBursts;
868  shotCounter = 0;
869  }
870  }
871  reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease);
872 
873  if (user != null)
874  {
875  reload /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed);
876  }
877 
878  if (projectile != null)
879  {
881  {
882  flipFiringOffset = !flipFiringOffset;
883  }
884  activeProjectiles.Add(projectile);
885  projectile.Drop(null, setTransform: false);
886  if (projectile.body != null)
887  {
888  projectile.body.Dir = 1.0f;
889  projectile.body.ResetDynamics();
890  projectile.body.Enabled = true;
891  }
892 
893  float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
894 
895  Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition());
896 
897  //check if there's some other sub between the turret's origin and the launch pos,
898  //and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub
899  Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true,
900  customPredicate: (Fixture f) =>
901  {
902  return f.Body.UserData is not Submarine sub || sub != item.Submarine;
903  });
904  if (pickedBody != null)
905  {
906  launchPos = Submarine.LastPickedPosition;
907  }
908  projectile.SetTransform(launchPos, -(launchRotation ?? Rotation) + spread);
909  projectile.UpdateTransform();
910  projectile.Submarine = projectile.body?.Submarine;
911 
912  Projectile projectileComponent = projectile.GetComponent<Projectile>();
913  if (projectileComponent != null)
914  {
915  TryDetermineProjectileSpeed(projectileComponent);
916  projectileComponent.Launcher = item;
917  projectileComponent.Attacker = projectileComponent.User = user;
918  if (projectileComponent.Attack != null)
919  {
920  projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
921  }
922  projectileComponent.Use(null, LaunchImpulse);
923  if (item.GetComponent<TriggerComponent>() is { } trigger)
924  {
925  projectileComponent.IgnoredBodies.Add(trigger.PhysicsBody.FarseerBody);
926  }
927  projectile.GetComponent<Rope>()?.Attach(item, projectile);
928  projectileComponent.User = user;
929 
930  if (item.Submarine != null && projectile.body != null)
931  {
932  Vector2 velocitySum = item.Submarine.PhysicsBody.LinearVelocity + projectile.body.LinearVelocity;
933  if (velocitySum.LengthSquared() < NetConfig.MaxPhysicsBodyVelocity * NetConfig.MaxPhysicsBodyVelocity * 0.9f)
934  {
935  projectile.body.LinearVelocity = velocitySum;
936  }
937  }
938  }
939 
940  projectile.Container?.RemoveContained(projectile);
941  }
942 #if SERVER
943  item.CreateServerEvent(this, new EventData(projectile, this));
944 #endif
945 
946  ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
947  LaunchProjSpecific();
948  }
949 
950  private void TryDetermineProjectileSpeed(Projectile projectile)
951  {
952  if (projectile != null && !projectile.Hitscan)
953  {
954  projectileSpeed =
955  ConvertUnits.ToDisplayUnits(
956  MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity));
957  }
958  }
959 
960  partial void LaunchProjSpecific();
961 
962  private static void ShiftItemsInProjectileContainer(ItemContainer container)
963  {
964  if (container == null) { return; }
965  bool moved;
966  do
967  {
968  moved = false;
969  for (int i = 1; i < container.Capacity; i++)
970  {
971  if (container.Inventory.GetItemAt(i) is Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1))
972  {
973  if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true))
974  {
975  moved = true;
976  }
977  }
978  }
979  } while (moved);
980  }
981 
982  private float waitTimer;
983  private float randomAimTimer;
984 
985  private float prevTargetRotation;
986  private float updateTimer;
987  private bool updatePending;
988 
989  private float GetTargetPriorityModifier() => currentChargingState == ChargingState.WindingUp ? 10f : AICurrentTargetPriorityMultiplier;
990 
991  public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag = default)
992  {
993  if (!ignorePower && !HasPowerToShoot())
994  {
995  return;
996  }
997 
998  IsActive = true;
999 
1000  if (friendlyTag.IsEmpty)
1001  {
1002  friendlyTag = FriendlyTag;
1003  }
1004 
1005  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
1006  {
1007  return;
1008  }
1009 
1010  if (updatePending)
1011  {
1012  if (updateTimer < 0.0f)
1013  {
1014 #if SERVER
1015  item.CreateServerEvent(this);
1016 #endif
1017  prevTargetRotation = targetRotation;
1018  updateTimer = 0.25f;
1019  }
1020  updateTimer -= deltaTime;
1021  }
1022 
1023  if (AimDelay && waitTimer > 0)
1024  {
1025  waitTimer -= deltaTime;
1026  return;
1027  }
1028  Submarine closestSub = null;
1029  float maxDistance = 10000.0f;
1030  float shootDistance = AIRange;
1031  ISpatialEntity target = null;
1032  float closestDist = shootDistance * shootDistance;
1033  if (TargetCharacters)
1034  {
1035  foreach (var character in Character.CharacterList)
1036  {
1037  if (!IsValidTarget(character)) { continue; }
1038  float priority = isSlowTurret ? character.Params.AISlowTurretPriority : character.Params.AITurretPriority;
1039  if (priority <= 0) { continue; }
1040  if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; }
1041  float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
1042  if (dist > closestDist) { continue; }
1043  if (!IsWithinAimingRadius(character.WorldPosition)) { continue; }
1044  target = character;
1045  if (currentTarget != null && target == currentTarget)
1046  {
1047  priority *= GetTargetPriorityModifier();
1048  }
1049  closestDist = dist / priority;
1050  }
1051  }
1052  if (TargetItems)
1053  {
1054  foreach (Item targetItem in Item.ItemList)
1055  {
1056  if (!IsValidTarget(targetItem)) { continue; }
1057  float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
1058  if (priority <= 0) { continue; }
1059  float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
1060  if (dist > closestDist) { continue; }
1061  if (dist > shootDistance * shootDistance) { continue; }
1062  if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
1063  if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
1064  target = targetItem;
1065  if (currentTarget != null && target == currentTarget)
1066  {
1067  priority *= GetTargetPriorityModifier();
1068  }
1069  closestDist = dist / priority;
1070  }
1071  }
1072  if (TargetSubmarines)
1073  {
1074  if (target == null || target.Submarine != null)
1075  {
1076  closestDist = maxDistance * maxDistance;
1077  foreach (Submarine sub in Submarine.Loaded)
1078  {
1079  if (sub == Item.Submarine) { continue; }
1080  if (item.Submarine != null)
1081  {
1082  if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; }
1083  }
1084  float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition);
1085  if (dist > closestDist) { continue; }
1086  closestSub = sub;
1087  closestDist = dist;
1088  }
1089  closestDist = shootDistance * shootDistance;
1090  if (closestSub != null)
1091  {
1092  foreach (var hull in Hull.HullList)
1093  {
1094  if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; }
1095  float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition);
1096  if (dist > closestDist) { continue; }
1097  // Don't check the angle, because it doesn't work on Thalamus spike. The angle check wouldn't be very important here anyway.
1098  target = hull;
1099  closestDist = dist;
1100  }
1101  }
1102  }
1103  }
1104 
1105  if (target == null && RandomMovement)
1106  {
1107  // Random movement while there's no target
1108  waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f);
1109  targetRotation = Rand.Range(minRotation, maxRotation);
1110  updatePending = true;
1111  return;
1112  }
1113 
1114  if (AimDelay)
1115  {
1116  if (RandomAimAmount > 0)
1117  {
1118  if (randomAimTimer < 0)
1119  {
1120  // Random disorder or other flaw in the targeting.
1121  randomAimTimer = Rand.Range(RandomAimMinTime, RandomAimMaxTime);
1122  waitTimer = Rand.Range(0.25f, 1f);
1123  float randomAim = MathHelper.ToRadians(RandomAimAmount);
1124  targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim));
1125  updatePending = true;
1126  return;
1127  }
1128  else
1129  {
1130  randomAimTimer -= deltaTime;
1131  }
1132  }
1133  }
1134  if (target == null) { return; }
1135  currentTarget = target;
1136 
1137  float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
1138  targetRotation = MathUtils.WrapAngleTwoPi(angle);
1139  if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; }
1140 
1141  if (target is Hull targetHull)
1142  {
1143  Vector2 barrelDir = GetBarrelDir();
1144  if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
1145  {
1146  return;
1147  }
1148  }
1149  else
1150  {
1151  if (!IsWithinAimingRadius(angle)) { return; }
1152  if (!IsPointingTowards(target.WorldPosition)) { return; }
1153  }
1154  Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
1155  Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
1156  // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
1157  Body worldTarget = CheckLineOfSight(start, end);
1158  bool shoot;
1159  if (target.Submarine != null)
1160  {
1161  start -= target.Submarine.SimPosition;
1162  end -= target.Submarine.SimPosition;
1163  Body transformedTarget = CheckLineOfSight(start, end);
1164  shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines));
1165  }
1166  else
1167  {
1168  shoot = CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines);
1169  }
1170  if (shoot)
1171  {
1172  TryLaunch(deltaTime, ignorePower: ignorePower);
1173  }
1174  }
1175 
1176  public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
1177  {
1178  if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
1179  {
1180  if (previousTarget.LastAttacker == null || previousTarget.LastAttacker == character)
1181  {
1182  character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
1183  identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
1184  minDurationBetweenSimilar: 5.0f);
1185  }
1186  character.AIController.SelectTarget(null);
1187  }
1188 
1189  bool canShoot = HasPowerToShoot();
1190  if (!canShoot)
1191  {
1192  float lowestCharge = 0.0f;
1193  PowerContainer batteryToLoad = null;
1194  foreach (PowerContainer battery in GetDirectlyConnectedBatteries())
1195  {
1196  if (!battery.Item.IsInteractable(character)) { continue; }
1197  if (battery.OutputDisabled) { continue; }
1198  if (batteryToLoad == null || battery.Charge < lowestCharge)
1199  {
1200  batteryToLoad = battery;
1201  lowestCharge = battery.Charge;
1202  }
1203  if (battery.Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(battery.Item, character))
1204  {
1205  if (battery.Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
1206  {
1207  objective.AddSubObjective(new AIObjectiveRepairItem(character, battery.Item, objective.objectiveManager, isPriority: true));
1208  return false;
1209  }
1210  else
1211  {
1212  character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
1213  identifier: "supercapacitorisbroken".ToIdentifier(),
1214  minDurationBetweenSimilar: 30.0f);
1215  }
1216  }
1217  }
1218  if (batteryToLoad == null) { return true; }
1219  if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
1220  {
1221  objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: Identifier.Empty, requireEquip: false));
1222  return false;
1223  }
1224  if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0)
1225  {
1226  character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
1227  identifier: "turrethasnopower".ToIdentifier(),
1228  minDurationBetweenSimilar: 30.0f);
1229  }
1230  }
1231 
1232  int usableProjectileCount = 0;
1233  int maxProjectileCount = 0;
1234  foreach (MapEntity e in item.linkedTo)
1235  {
1236  if (!item.IsInteractable(character)) { continue; }
1237  if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
1238  if (e is Item projectileContainer)
1239  {
1240  var container = projectileContainer.GetComponent<ItemContainer>();
1241  if (container != null)
1242  {
1243  maxProjectileCount += container.Capacity;
1244  var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f);
1245  var firstProjectile = projectiles.FirstOrDefault();
1246 
1247  if (firstProjectile?.Prefab != previousAmmo?.Prefab)
1248  {
1249  //assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on,
1250  //and if the projectile type isn't the same as before
1251  projectileSpeed = float.PositiveInfinity;
1252  }
1253  previousAmmo = firstProjectile;
1254  if (projectiles.Any())
1255  {
1256  var projectile =
1257  firstProjectile.GetComponent<Projectile>() ??
1258  firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent<Projectile>();
1259  TryDetermineProjectileSpeed(projectile);
1260  usableProjectileCount += projectiles.Count();
1261  }
1262  }
1263  }
1264  }
1265 
1266  if (usableProjectileCount == 0)
1267  {
1268  ItemContainer container = null;
1269  Item containerItem = null;
1270  foreach (MapEntity e in item.linkedTo)
1271  {
1272  containerItem = e as Item;
1273  if (containerItem == null) { continue; }
1274  if (!containerItem.IsInteractable(character)) { continue; }
1275  if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; }
1276  container = containerItem.GetComponent<ItemContainer>();
1277  if (container != null) { break; }
1278  }
1279  if (container == null || !container.ContainableItemIdentifiers.Any())
1280  {
1281  if (character.IsOnPlayerTeam)
1282  {
1283  character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value,
1284  identifier: "cannotloadturret".ToIdentifier(),
1285  minDurationBetweenSimilar: 30.0f);
1286  }
1287  return true;
1288  }
1289  if (objective.SubObjectives.None())
1290  {
1291  var loadItemsObjective = AIContainItems<Turret>(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true);
1292  loadItemsObjective.ignoredContainerIdentifiers = ((MapEntity)containerItem).Prefab.Identifier.ToEnumerable().ToImmutableHashSet();
1293  if (character.IsOnPlayerTeam)
1294  {
1295  character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value,
1296  identifier: "loadturret".ToIdentifier(),
1297  minDurationBetweenSimilar: 30.0f);
1298  }
1299  loadItemsObjective.Abandoned += CheckRemainingAmmo;
1300  loadItemsObjective.Completed += CheckRemainingAmmo;
1301  return false;
1302 
1303  void CheckRemainingAmmo()
1304  {
1305  if (!character.IsOnPlayerTeam) { return; }
1306  if (character.Submarine != Submarine.MainSub) { return; }
1307  Identifier ammoType = container.ContainableItemIdentifiers.FirstOrNull() ?? "ammobox".ToIdentifier();
1308  int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
1309  if (remainingAmmo == 0)
1310  {
1311  character.Speak(TextManager.Get($"DialogOutOf{ammoType}", "DialogOutOfTurretAmmo").Value,
1312  identifier: "outofammo".ToIdentifier(),
1313  minDurationBetweenSimilar: 30.0f);
1314  }
1315  else if (remainingAmmo < 3)
1316  {
1317  character.Speak(TextManager.Get($"DialogLowOn{ammoType}").Value,
1318  identifier: "outofammo".ToIdentifier(),
1319  minDurationBetweenSimilar: 30.0f);
1320  }
1321  }
1322  }
1323  if (objective.SubObjectives.Any())
1324  {
1325  return false;
1326  }
1327  }
1328 
1329  //enough shells and power
1330  Character closestEnemy = null;
1331  Vector2? targetPos = null;
1332  float maxDistance = 10000;
1333  float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
1334  float closestDistance = maxDistance * maxDistance;
1335  bool hadCurrentTarget = currentTarget != null;
1336  if (hadCurrentTarget)
1337  {
1338  bool isValidTarget = IsValidTarget(currentTarget);
1339  if (isValidTarget)
1340  {
1341  float dist = Vector2.DistanceSquared(item.WorldPosition, currentTarget.WorldPosition);
1342  if (dist > closestDistance)
1343  {
1344  isValidTarget = false;
1345  }
1346  else if (currentTarget is Item targetItem)
1347  {
1348  if (!IsTargetItemCloseEnough(targetItem, dist))
1349  {
1350  isValidTarget = false;
1351  }
1352  }
1353  }
1354  if (!isValidTarget)
1355  {
1356  currentTarget = null;
1357  aiFindTargetTimer = CrewAIFindTargetMinInverval;
1358  }
1359  }
1360  if (aiFindTargetTimer <= 0.0f)
1361  {
1362  foreach (Character enemy in Character.CharacterList)
1363  {
1364  if (!IsValidTarget(enemy)) { continue; }
1365  float priority = isSlowTurret ? enemy.Params.AISlowTurretPriority : enemy.Params.AITurretPriority;
1366  if (priority <= 0) { continue; }
1367  if (character.Submarine != null)
1368  {
1369  if (enemy.Submarine == character.Submarine) { continue; }
1370  if (enemy.Submarine != null)
1371  {
1372  if (enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
1373  if (enemy.Submarine.Info.IsOutpost) { continue; }
1374  }
1375  }
1376  // Don't aim monsters that are inside any submarine.
1377  if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
1378  if (HumanAIController.IsFriendly(character, enemy)) { continue; }
1379  // Don't shoot at captured enemies.
1380  if (enemy.LockHands) { continue; }
1381  float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
1382  if (dist > closestDistance) { continue; }
1383  if (dist < shootDistance * shootDistance)
1384  {
1385  // Only check the angle to targets that are close enough to be shot at
1386  // We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
1387  if (!IsWithinAimingRadius(enemy.WorldPosition)) { continue; }
1388  }
1389  if (currentTarget != null && enemy == currentTarget)
1390  {
1391  priority *= GetTargetPriorityModifier();
1392  }
1393  targetPos = enemy.WorldPosition;
1394  closestEnemy = enemy;
1395  closestDistance = dist / priority;
1396  currentTarget = closestEnemy;
1397  }
1398  foreach (Item targetItem in Item.ItemList)
1399  {
1400  if (!IsValidTarget(targetItem)) { continue; }
1401  float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
1402  if (priority <= 0) { continue; }
1403  float dist = Vector2.DistanceSquared(item.WorldPosition, targetItem.WorldPosition);
1404  if (dist > closestDistance) { continue; }
1405  if (dist > shootDistance * shootDistance) { continue; }
1406  if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
1407  if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
1408  if (currentTarget != null && targetItem == currentTarget)
1409  {
1410  priority *= GetTargetPriorityModifier();
1411  }
1412  targetPos = targetItem.WorldPosition;
1413  closestDistance = dist / priority;
1414  // Override the target character so that we can target the item instead.
1415  closestEnemy = null;
1416  currentTarget = targetItem;
1417  }
1418  aiFindTargetTimer = currentTarget == null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval;
1419  }
1420  else if (currentTarget != null)
1421  {
1422  targetPos = currentTarget.WorldPosition;
1423  }
1424  bool iceSpireSpotted = false;
1425  Vector2 targetVelocity = Vector2.Zero;
1426  // Adjust the target character position (limb or submarine)
1427  if (currentTarget is Character targetCharacter)
1428  {
1429  //if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
1430  if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
1431  {
1432  targetPos = targetCharacter.CurrentHull.WorldPosition;
1433  if (closestDistance > maxDistance * maxDistance)
1434  {
1435  ResetTarget();
1436  }
1437  }
1438  else
1439  {
1440  // Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
1441  float closestDistSqr = closestDistance;
1442  foreach (Limb limb in targetCharacter.AnimController.Limbs)
1443  {
1444  if (limb.IsSevered) { continue; }
1445  if (limb.Hidden) { continue; }
1446  if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
1447  float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
1448  if (distSqr < closestDistSqr)
1449  {
1450  closestDistSqr = distSqr;
1451  if (limb == targetCharacter.AnimController.MainLimb)
1452  {
1453  //prefer main limb (usually a much better target than the extremities that are often the closest limbs)
1454  closestDistSqr *= 0.5f;
1455  }
1456  targetPos = limb.WorldPosition;
1457  }
1458  }
1459  if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue)
1460  {
1461  //lead the target (aim where the target will be in the future)
1462  float dist = MathF.Sqrt(closestDistSqr);
1463  float projectileMovementTime = dist / projectileSpeed;
1464 
1465  targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
1466  Vector2 movementAmount = targetVelocity * projectileMovementTime;
1467  //don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that,
1468  //it'd most likely fail anyway
1469  movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead));
1470  Vector2 futurePosition = targetPos.Value + movementAmount;
1471  targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character));
1472  }
1473  if (closestDistSqr > shootDistance * shootDistance)
1474  {
1475  aiFindTargetTimer = CrewAIFindTargetMinInverval;
1476  ResetTarget();
1477  }
1478  }
1479  void ResetTarget()
1480  {
1481  // Not close enough to shoot.
1482  currentTarget = null;
1483  closestEnemy = null;
1484  targetPos = null;
1485  }
1486  }
1487  else if (targetPos == null && item.Submarine != null && Level.Loaded != null)
1488  {
1489  // Check ice spires
1490  shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
1491  closestDistance = shootDistance;
1492  foreach (var wall in Level.Loaded.ExtraWalls)
1493  {
1494  if (wall is not DestructibleLevelWall destructibleWall || destructibleWall.Destroyed) { continue; }
1495  foreach (var cell in wall.Cells)
1496  {
1497  if (!cell.DoesDamage) { continue; }
1498  foreach (var edge in cell.Edges)
1499  {
1500  Vector2 p1 = edge.Point1 + cell.Translation;
1501  Vector2 p2 = edge.Point2 + cell.Translation;
1502  Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition);
1503  if (!IsWithinAimingRadius(closestPoint))
1504  {
1505  // The closest point can't be targeted -> get a point directly in front of the turret
1506  Vector2 barrelDir = new Vector2((float)Math.Cos(Rotation), -(float)Math.Sin(Rotation));
1507  if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
1508  {
1509  closestPoint = intersection;
1510  if (!IsWithinAimingRadius(closestPoint)) { continue; }
1511  }
1512  else
1513  {
1514  continue;
1515  }
1516  }
1517  float dist = Vector2.Distance(closestPoint, item.WorldPosition);
1518 
1519  //add one px to make sure the visibility raycast doesn't miss the cell due to the end position being right at the edge of the cell
1520  closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1);
1521 
1522  if (dist > AIRange + 1000) { continue; }
1523  float dot = 0;
1524  if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero))
1525  {
1526  dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition));
1527  }
1528  float minAngle = 0.5f;
1529  if (dot < minAngle && dist > 1000)
1530  {
1531  // The sub is not moving towards the target and it's not very close to the turret either -> ignore
1532  continue;
1533  }
1534  // Allow targeting farther when heading towards the spire (up to 1000 px)
1535  dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot));
1536  if (dist > closestDistance) { continue; }
1537  targetPos = closestPoint;
1538  closestDistance = dist;
1539  iceSpireSpotted = true;
1540  }
1541  }
1542  }
1543  }
1544 
1545  if (targetPos == null) { return false; }
1546  // Force the highest priority so that we don't change the objective while targeting enemies.
1547  objective.ForceHighestPriority = true;
1548 #if CLIENT
1549  debugDrawTargetPos = targetPos.Value;
1550 #endif
1551  if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
1552  {
1553  if (character.IsOnPlayerTeam)
1554  {
1555  if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget)
1556  {
1557  if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman)
1558  {
1559  character.Speak(TextManager.Get("DialogNewTargetSpotted").Value,
1560  identifier: "newtargetspotted".ToIdentifier(),
1561  minDurationBetweenSimilar: 30.0f);
1562  }
1563  else if (CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName))
1564  {
1565  character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value,
1566  identifier: "identifiedtargetspotted".ToIdentifier(),
1567  minDurationBetweenSimilar: 30.0f);
1568  }
1569  else
1570  {
1571  character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value,
1572  identifier: "unidentifiedtargetspotted".ToIdentifier(),
1573  minDurationBetweenSimilar: 5.0f);
1574  }
1575  }
1576  else if (!CreatureMetrics.Encountered.Contains(closestEnemy.SpeciesName))
1577  {
1578  character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value,
1579  identifier: "unidentifiedtargetspotted".ToIdentifier(),
1580  minDurationBetweenSimilar: 5.0f);
1581  }
1582  CreatureMetrics.AddEncounter(closestEnemy.SpeciesName);
1583  }
1584  character.AIController.SelectTarget(closestEnemy.AiTarget);
1585  }
1586  else if (iceSpireSpotted && character.IsOnPlayerTeam)
1587  {
1588  character.Speak(TextManager.Get("DialogIceSpireSpotted").Value,
1589  identifier: "icespirespotted".ToIdentifier(),
1590  minDurationBetweenSimilar: 60.0f);
1591  }
1592 
1593  character.CursorPosition = targetPos.Value;
1594  if (character.Submarine != null)
1595  {
1596  character.CursorPosition -= character.Submarine.Position;
1597  }
1598 
1599  if (IsPointingTowards(targetPos.Value))
1600  {
1601  Vector2 barrelDir = GetBarrelDir();
1602  Vector2 aimStartPos = item.WorldPosition;
1603  Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance;
1604  bool allowShootingIfNothingInWay = false;
1605  if (currentTarget != null)
1606  {
1607  Vector2 targetStartPos = currentTarget.WorldPosition;
1608  Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead);
1609 
1610  //if there's nothing in the way (not even the target we're trying to aim towards),
1611  //shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air"
1612  allowShootingIfNothingInWay =
1613  targetVelocity.LengthSquared() > 0.001f &&
1614  MathUtils.LineSegmentsIntersect(
1615  aimStartPos, aimEndPos,
1616  targetStartPos, targetEndPos) &&
1617  //target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense
1618  Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f;
1619  }
1620 
1621  Vector2 start = ConvertUnits.ToSimUnits(aimStartPos);
1622  Vector2 end = ConvertUnits.ToSimUnits(aimEndPos);
1623  // Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
1624  Body worldTarget = CheckLineOfSight(start, end);
1625  if (closestEnemy != null && closestEnemy.Submarine != null)
1626  {
1627  start -= closestEnemy.Submarine.SimPosition;
1628  end -= closestEnemy.Submarine.SimPosition;
1629  Body transformedTarget = CheckLineOfSight(start, end);
1630  canShoot =
1631  CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) &&
1632  (worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay));
1633  }
1634  else
1635  {
1636  canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay);
1637  }
1638  if (!canShoot) { return false; }
1639  if (character.IsOnPlayerTeam)
1640  {
1641  character.Speak(TextManager.Get("DialogFireTurret").Value,
1642  identifier: "fireturret".ToIdentifier(),
1643  minDurationBetweenSimilar: 30.0f);
1644  }
1645  character.SetInput(InputType.Shoot, true, true);
1646  }
1647  return false;
1648  }
1649 
1650  private bool IsPointingTowards(Vector2 targetPos)
1651  {
1652  float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition);
1653  float turretAngle = -Rotation;
1654  float maxAngleError = MathHelper.ToRadians(MaxAngleOffset);
1655  if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
1656  {
1657  //larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
1658  maxAngleError *= 2.0f;
1659  }
1660  return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError;
1661  }
1662 
1663  private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
1664 
1668  public override float GetCurrentPowerConsumption(Connection conn = null)
1669  {
1670  return 0;
1671  }
1672 
1673  // Not exahustive, but helps to get rid of some code duplication
1674  private static bool IsValidTarget(ISpatialEntity target)
1675  {
1676  if (target == null) { return false; }
1677  if (target is Character targetCharacter)
1678  {
1679  if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0)
1680  {
1681  return false;
1682  }
1683  }
1684  else if (target is Item targetItem)
1685  {
1686  if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.IsHidden)
1687  {
1688  return false;
1689  }
1690  if (targetItem.Submarine != null)
1691  {
1692  return false;
1693  }
1694  if (targetItem.ParentInventory != null)
1695  {
1696  return false;
1697  }
1698  }
1699  return true;
1700  }
1701 
1702  private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
1703  {
1704  if (!friendlyTag.IsEmpty)
1705  {
1706  if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
1707  }
1708  if (FriendlyTeam != CharacterTeamType.None)
1709  {
1710  if (target.TeamID == FriendlyTeam) { return false; }
1711  }
1712  bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
1713  if (isHuman)
1714  {
1715  if (item.Submarine != null)
1716  {
1717  // Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
1718  var turretTeam = FriendlyTeam == CharacterTeamType.None ? item.Submarine.TeamID : FriendlyTeam;
1719  return !target.IsOnFriendlyTeam(turretTeam) && TargetHumans;
1720  }
1721  return TargetHumans;
1722  }
1723  else
1724  {
1725  // Shouldn't check the team here, because all the enemies are in the same team (None).
1726  return TargetMonsters;
1727  }
1728  }
1729 
1730  private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false)
1731  {
1732  if (targetBody == null)
1733  {
1734  //nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air
1735  return allowShootingIfNothingInWay;
1736  }
1737  Character targetCharacter = null;
1738  if (targetBody.UserData is Character c)
1739  {
1740  targetCharacter = c;
1741  }
1742  else if (targetBody.UserData is Limb limb)
1743  {
1744  targetCharacter = limb.character;
1745  }
1746  if (targetCharacter != null && !targetCharacter.Removed)
1747  {
1748  if (user != null)
1749  {
1750  if (HumanAIController.IsFriendly(user, targetCharacter))
1751  {
1752  return false;
1753  }
1754  }
1755  else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag))
1756  {
1757  // Note that Thalamus runs this even when AutoOperate is false.
1758  return false;
1759  }
1760  }
1761  else
1762  {
1763  if (targetBody.UserData is ISpatialEntity e)
1764  {
1765  if (e is Structure { Indestructible: true }) { return false; }
1766  if (!targetSubmarines && e is Submarine) { return false; }
1767  Submarine sub = e.Submarine ?? e as Submarine;
1768  if (sub == null) { return true; }
1769  if (sub == Item.Submarine) { return false; }
1770  if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
1771  if (sub.TeamID == Item.Submarine.TeamID) { return false; }
1772  }
1773  else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
1774  {
1775  // Hit something else, probably a level wall
1776  return false;
1777  }
1778  }
1779  return true;
1780  }
1781 
1782  private Body CheckLineOfSight(Vector2 start, Vector2 end)
1783  {
1784  var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionProjectile;
1785  Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
1786  customPredicate: (Fixture f) =>
1787  {
1788  if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
1789  if (f.UserData is Hull) { return false; }
1790  return !item.StaticFixtures.Contains(f);
1791  });
1792  return pickedBody;
1793  }
1794 
1795  private Vector2 GetRelativeFiringPosition(bool useOffset = true)
1796  {
1797  Vector2 transformedFiringOffset = Vector2.Zero;
1798  if (useOffset)
1799  {
1800  Vector2 currOffSet = FiringOffset;
1801  if (flipFiringOffset) { currOffSet.X = -currOffSet.X; }
1802  transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -Rotation);
1803  }
1804  return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
1805  }
1806 
1807  private bool IsWithinAimingRadius(float angle)
1808  {
1809  float midRotation = (minRotation + maxRotation) / 2.0f;
1810  while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
1811  while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; }
1812  return angle >= minRotation && angle <= maxRotation;
1813  }
1814 
1815  public bool IsWithinAimingRadius(Vector2 target) => IsWithinAimingRadius(-MathUtils.VectorToAngle(target - item.WorldPosition));
1816 
1817  protected override void RemoveComponentSpecific()
1818  {
1819  base.RemoveComponentSpecific();
1820 
1821  barrelSprite?.Remove(); barrelSprite = null;
1822  railSprite?.Remove(); railSprite = null;
1823  barrelSpriteBroken?.Remove(); barrelSpriteBroken = null;
1824  railSpriteBroken?.Remove(); railSpriteBroken = null;
1825 
1826 #if CLIENT
1827  crosshairSprite?.Remove(); crosshairSprite = null;
1828  crosshairPointerSprite?.Remove(); crosshairPointerSprite = null;
1829  moveSoundChannel?.Dispose(); moveSoundChannel = null;
1830  WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null;
1831  if (powerIndicator != null)
1832  {
1833  powerIndicator.RectTransform.Parent = null;
1834  powerIndicator = null;
1835  }
1836 #endif
1837  }
1838 
1839  private List<Projectile> GetLoadedProjectiles()
1840  {
1841  List<Projectile> projectiles = new List<Projectile>();
1842  // check the item itself first
1843  CheckProjectileContainer(item, projectiles, out bool _);
1844  for (int j = 0; j < item.linkedTo.Count; j++)
1845  {
1846  var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
1847  if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
1848  if (e is Item projectileContainer)
1849  {
1850  CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching);
1851  if (projectiles.Any() || stopSearching) { return projectiles; }
1852  }
1853  }
1854  return projectiles;
1855  }
1856 
1857  private static void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
1858  {
1859  stopSearching = false;
1860  if (projectileContainer.Condition <= 0.0f) { return; }
1861 
1862  var containedItems = projectileContainer.ContainedItems;
1863  if (containedItems == null) { return; }
1864 
1865  foreach (Item containedItem in containedItems)
1866  {
1867  var projectileComponent = containedItem.GetComponent<Projectile>();
1868  if (projectileComponent != null && projectileComponent.Item.body != null)
1869  {
1870  projectiles.Add(projectileComponent);
1871  return;
1872  }
1873  else
1874  {
1875  //check if the contained item is another itemcontainer with projectiles inside it
1876  foreach (Item subContainedItem in containedItem.ContainedItems)
1877  {
1878  projectileComponent = subContainedItem.GetComponent<Projectile>();
1879  if (projectileComponent != null && projectileComponent.Item.body != null)
1880  {
1881  projectiles.Add(projectileComponent);
1882  }
1883  }
1884  // in the case that we found a container that still has condition/ammo left,
1885  // return and inform GetLoadedProjectiles to stop searching past this point (even if no projectiles were not found)
1886  if (containedItem.Condition > 0.0f || projectiles.Any())
1887  {
1888  stopSearching = true;
1889  return;
1890  }
1891  }
1892  }
1893  }
1894 
1895  public override void FlipX(bool relativeToSub)
1896  {
1897  minRotation = MathHelper.Pi - minRotation;
1898  maxRotation = MathHelper.Pi - maxRotation;
1899 
1900  var temp = minRotation;
1901  minRotation = maxRotation;
1902  maxRotation = temp;
1903 
1904  barrelPos.X = item.Rect.Width / item.Scale - barrelPos.X;
1905 
1906  while (minRotation < 0)
1907  {
1908  minRotation += MathHelper.TwoPi;
1909  maxRotation += MathHelper.TwoPi;
1910  }
1911  targetRotation = Rotation = (minRotation + maxRotation) / 2;
1912 
1913  UpdateTransformedBarrelPos();
1915  }
1916 
1917  public override void FlipY(bool relativeToSub)
1918  {
1919  BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(180 - BaseRotation)));
1920 
1921  minRotation = -minRotation;
1922  maxRotation = -maxRotation;
1923 
1924  var temp = minRotation;
1925  minRotation = maxRotation;
1926  maxRotation = temp;
1927 
1928  while (minRotation < 0)
1929  {
1930  minRotation += MathHelper.TwoPi;
1931  maxRotation += MathHelper.TwoPi;
1932  }
1933  targetRotation = Rotation = (minRotation + maxRotation) / 2;
1934 
1935  UpdateTransformedBarrelPos();
1937  }
1938 
1939  public override void ReceiveSignal(Signal signal, Connection connection)
1940  {
1941  Character sender = signal.sender;
1942  switch (connection.Name)
1943  {
1944  case "position_in":
1945  if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation))
1946  {
1947  if (!MathUtils.IsValid(newRotation)) { return; }
1948  targetRotation = MathHelper.ToRadians(newRotation);
1949  IsActive = true;
1950  }
1951  user = sender;
1952  ActiveUser = sender;
1953  resetActiveUserTimer = 1f;
1954  resetUserTimer = 10.0f;
1955  break;
1956  case "trigger_in":
1957  if (signal.value == "0") { return; }
1958  item.Use((float)Timing.Step, user: sender);
1959  user = sender;
1960  ActiveUser = sender;
1961  resetActiveUserTimer = 1f;
1962  resetUserTimer = 10.0f;
1963  //triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character
1964  //so lets do it manually
1965  if (!characterUsable && sender != null)
1966  {
1967  TryLaunch((float)Timing.Step, sender);
1968  }
1969  break;
1970  case "toggle_light":
1971  if (lightComponents != null && signal.value != "0")
1972  {
1973  foreach (var light in lightComponents)
1974  {
1975  light.IsOn = !light.IsOn;
1976  }
1978  }
1979  break;
1980  case "set_light":
1981  if (lightComponents != null)
1982  {
1983  bool shouldBeOn = signal.value != "0";
1984  foreach (var light in lightComponents)
1985  {
1986  light.IsOn = shouldBeOn;
1987  }
1989  }
1990  break;
1991  case SetAutoOperateConnection:
1992  if (!AllowAutoOperateWithWiring) { return; }
1993  AutoOperate = signal.value != "0";
1994  break;
1995  case ToggleAutoOperateConnection:
1996  if (!AllowAutoOperateWithWiring) { return; }
1997  if (signal.value != "0")
1998  {
2000  }
2001  break;
2002  }
2003  }
2004 
2005  private Vector2? loadedRotationLimits;
2006  private float? loadedBaseRotation;
2007  public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
2008  {
2009  base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
2010  loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
2011  loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
2012  }
2013 
2014  public override void OnItemLoaded()
2015  {
2016  base.OnItemLoaded();
2017  FindLightComponents();
2018  targetRotation = Rotation;
2019  if (!loadedBaseRotation.HasValue)
2020  {
2021  if (item.FlippedX) { FlipX(relativeToSub: false); }
2022  if (item.FlippedY) { FlipY(relativeToSub: false); }
2023  }
2024  }
2025 
2026  public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
2027  {
2028  if (TryExtractEventData(extraData, out EventData eventData))
2029  {
2030  msg.WriteUInt16(eventData.Projectile?.ID ?? LaunchWithoutProjectileId);
2031  msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(Rotation), minRotation, maxRotation), minRotation, maxRotation, 16);
2032  }
2033  else
2034  {
2035  msg.WriteUInt16((ushort)0);
2036  msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(targetRotation), minRotation, maxRotation), minRotation, maxRotation, 16);
2037  }
2038 
2039  float wrapAngle(float angle)
2040  {
2041  float wrappedAngle = angle;
2042  while (wrappedAngle < minRotation && MathUtils.IsValid(wrappedAngle))
2043  {
2044  wrappedAngle += MathHelper.TwoPi;
2045  }
2046  while (wrappedAngle > maxRotation && MathUtils.IsValid(wrappedAngle))
2047  {
2048  wrappedAngle -= MathHelper.TwoPi;
2049  }
2050  return wrappedAngle;
2051  }
2052  }
2053  }
2054 }
2055 
2056 
virtual void SelectTarget(AITarget target)
void AddSubObjective(AIObjective objective, bool addFirst=false)
Definition: AIObjective.cs:206
readonly AIObjectiveManager objectiveManager
Definition: AIObjective.cs:113
override bool IsValidTarget(Item item)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
float GetStatValue(StatTypes statType, bool includeSaved=true)
static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility=false, float maxGain=2f, bool forceNotification=false)
Increases the characters skill at a rate proportional to their current skill. If you want to increase...
float GetAttributeFloat(string key, float def)
Vector2 GetAttributeVector2(string key, in Vector2 def)
AITarget AiTarget
Definition: Entity.cs:55
virtual Vector2 WorldPosition
Definition: Entity.cs:49
Submarine Submarine
Definition: Entity.cs:53
static NetworkMember NetworkMember
Definition: GameMain.cs:41
static GameSession GameSession
Definition: GameMain.cs:45
CampaignMode? Campaign
Definition: GameSession.cs:128
static readonly List< Hull > HullList
readonly List< Item > IgnoredItems
static bool IsFriendly(Character me, Character other, bool onlySameTeam=false)
bool IsShootable
Should the item's Use method be called with the "Use" or with the "Shoot" key?
void Use(float deltaTime, Character user=null, Limb targetLimb=null, Entity useTarget=null, Character userForOnUsedEvent=null)
bool IsInteractable(Character character)
Returns interactibility based on whether the character is on a player team
bool RequireAimToUse
If true, the user has to hold the "aim" key before use is registered. False by default.
override string Name
Note that this is not a LocalizedString instance, just the current name of the item as a string....
static readonly List< Item > ItemList
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)
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
IEnumerable< PowerContainer > GetDirectlyConnectedBatteries()
Definition: Powered.cs:699
float powerConsumption
The maximum amount of power the item can draw from connected items
Definition: Powered.cs:94
float GetAvailableInstantaneousBatteryPower()
Returns the amount of power that can be supplied by batteries directly connected to the item
Definition: Powered.cs:681
override void OnItemLoaded()
Called when all the components of the item have been loaded. Use to initialize connections between co...
Definition: Turret.cs:2014
override float GetCurrentPowerConsumption(Connection conn=null)
Turret doesn't consume grid power, directly takes from the batteries on its grid instead.
Definition: Turret.cs:1668
CharacterTeamType FriendlyTeam
Definition: Turret.cs:329
override void Update(float deltaTime, Camera cam)
Definition: Turret.cs:438
void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData=null)
Definition: Turret.cs:2026
override void FlipX(bool relativeToSub)
Definition: Turret.cs:1895
void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag=default)
Definition: Turret.cs:991
bool IsWithinAimingRadius(Vector2 target)
override void RemoveComponentSpecific()
Definition: Turret.cs:1817
Turret(Item item, ContentXElement element)
Definition: Turret.cs:335
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
Definition: Turret.cs:1176
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
Definition: Turret.cs:2007
override void FlipY(bool relativeToSub)
Definition: Turret.cs:1917
override void ReceiveSignal(Signal signal, Connection connection)
Definition: Turret.cs:1939
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
Definition: Turret.cs:386
IEnumerable< Item > ActiveProjectiles
Definition: Turret.cs:55
override bool Use(float deltaTime, Character character=null)
Definition: Turret.cs:613
static bool IsLoadedFriendlyOutpost
Is there a loaded level set, and is it a friendly outpost (FriendlyNPC or Team1). Does not take reput...
List< LevelWall > ExtraWalls
Special wall chunks that aren't part of the normal level geometry: includes things like the ocean flo...
bool? IsSevered
Definition: Limb.cs:351
Vector2?? WorldPosition
Definition: Limb.cs:401
bool Hidden
Definition: Limb.cs:388
Rectangle? WorldRect
Definition: MapEntity.cs:105
readonly MapEntityPrefab Prefab
Definition: MapEntity.cs:17
virtual Rectangle Rect
Definition: MapEntity.cs:99
readonly List< MapEntity > linkedTo
Definition: MapEntity.cs:52
bool IsLinkAllowed(MapEntityPrefab target)
static string CharacterLogName(Character character)
Definition: GameServer.cs:4602
static void Log(string line, ServerLog.MessageType messageType)
Definition: GameServer.cs:4609
static Screen Selected
Definition: Screen.cs:5
float SkillIncreasePerSecondWhenOperatingTurret
static SkillSettings Current
void Remove()
Definition: Sprite.cs:214
List< Item > GetItems(bool alsoFromConnectedSubs)
Submarine(SubmarineInfo info, bool showErrorMessages=true, Func< Submarine, List< MapEntity >> loadEntities=null, IdRemap linkedRemap=null)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)
Interface for entities that the server can send events to the clients
void WriteRangedSingle(Single val, Single min, Single max, int bitCount)
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195
@ Character
Characters only