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