3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Globalization;
9 using FarseerPhysics.Dynamics;
10 using System.Collections.Immutable;
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>();
24 const ushort LaunchWithoutProjectileId = ushort.MaxValue;
26 private Vector2 barrelPos;
27 private Vector2 transformedBarrelPos;
29 private float targetRotation;
32 private int shotCounter;
34 private float minRotation, maxRotation;
38 private float angularVelocity;
40 private int failedLaunchAttempts;
42 private float currentChargeTime;
43 private bool tryingToCharge;
45 private enum ChargingState
52 private ChargingState currentChargingState;
54 private readonly List<Item> activeProjectiles =
new List<Item>();
59 private float resetUserTimer;
61 private float aiFindTargetTimer;
63 private const float CrewAiFindTargetMaxInterval = 1.0f;
64 private const float CrewAIFindTargetMinInverval = 0.2f;
71 private const float MinimumProjectileVelocityForAimAhead = 20.0f;
77 private const float MaximumAimAhead = 10.0f;
79 private float projectileSpeed;
80 private Item previousAmmo;
82 private int currentLoaderIndex;
84 private const float TinkeringPowerCostReduction = 0.2f;
85 private const float TinkeringDamageIncrease = 0.2f;
86 private const float TinkeringReloadDecrease = 0.2f;
89 private float resetActiveUserTimer;
91 private List<LightComponent> lightComponents;
95 private readonly
bool isSlowTurret;
99 [
Serialize(
"0,0",
IsPropertySaveable.No, description:
"The position of the barrel relative to the upper left corner of the base sprite (in pixels).")]
109 UpdateTransformedBarrelPos();
113 [
Serialize(
"0,0",
IsPropertySaveable.No, description:
"The projectile launching location relative to transformed barrel position (in pixels).")]
116 private bool flipFiringOffset;
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.)")]
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).")]
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).")]
135 [
Serialize(0.0f,
IsPropertySaveable.No, description:
"Random spread applied to the firing angle of the projectiles (in degrees).")]
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)]
153 UpdateTransformedBarrelPos();
157 [
Serialize(3500.0f,
IsPropertySaveable.Yes, description:
"How close to a target the turret has to be for an AI character to fire it.")]
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.")]
164 get => _maxAngleOffset;
165 private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f);
168 [
Serialize(1.1f,
IsPropertySaveable.No, description:
"How much does the AI prefer currently selected targets over new targets closer to the turret.")]
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.")]
174 [
Serialize(0f,
IsPropertySaveable.Yes, description:
"The time required for a charge-type turret to charge up before able to fire.")]
177 #region Editable properties
180 Editable(0.0f, 1000.0f, decimals: 3)]
183 [
Serialize(1,
IsPropertySaveable.No, description:
"How many projectiles needs to be shot before we add an extra break? Think of the double coilgun."),
188 Editable(0.0f, 1000.0f, decimals: 3)]
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."),
199 [
Serialize(0f,
IsPropertySaveable.No, description:
"How long the barrell stays in place after the recoil and before retracting back to the original position."),
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)]
209 return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation));
213 float newMinRotation = MathHelper.ToRadians(value.X);
214 float newMaxRotation = MathHelper.ToRadians(value.Y);
216 bool minRotationModified = MathHelper.Distance(newMinRotation, minRotation) > 0.02f;
217 bool maxRotationModified = MathHelper.Distance(newMaxRotation, maxRotation) > 0.02f;
220 if (minRotationModified && !maxRotationModified)
222 newMinRotation = MathHelper.Clamp(newMinRotation, maxRotation - MathHelper.TwoPi, maxRotation);
224 else if (!minRotationModified && maxRotationModified)
226 newMaxRotation = MathHelper.Clamp(newMaxRotation, minRotation, minRotation + MathHelper.TwoPi);
229 maxRotation = newMaxRotation;
230 minRotation = newMinRotation;
232 Rotation = (minRotation + maxRotation) / 2;
234 if (lightComponents !=
null)
236 foreach (var light
in lightComponents)
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)]
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)]
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)]
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)]
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)]
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)]
270 [
Serialize(
"0,0,0,0",
IsPropertySaveable.Yes, description:
"Optional screen tint color when the item is being operated (R,G,B,A)."),
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)]
279 [
Serialize(
false,
IsPropertySaveable.Yes, description:
"Can the Auto Operate functionality be enabled using signals to the turret?"),
280 Editable(TransferToSwappedItem =
true)]
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)]
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)]
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)]
296 Editable(TransferToSwappedItem =
true)]
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)]
304 Editable(TransferToSwappedItem =
true)]
308 Editable(TransferToSwappedItem =
true)]
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)]
316 Editable(TransferToSwappedItem =
true)]
320 Editable(TransferToSwappedItem =
true)]
323 [
Serialize(
"",
IsPropertySaveable.Yes, description:
"[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."),
324 Editable(TransferToSwappedItem =
true)]
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)]
332 private const string SetAutoOperateConnection =
"set_auto_operate";
333 private const string ToggleAutoOperateConnection =
"toggle_auto_operate";
336 : base(
item, element)
340 foreach (var subElement
in element.Elements())
342 switch (subElement.Name.ToString().ToLowerInvariant())
345 barrelSprite =
new Sprite(subElement);
348 railSprite =
new Sprite(subElement);
350 case "barrelspritebroken":
351 barrelSpriteBroken =
new Sprite(subElement);
353 case "railspritebroken":
354 railSpriteBroken =
new Sprite(subElement);
357 chargeSprites.Add((
new Sprite(subElement), subElement.GetAttributeVector2(
"chargetarget", Vector2.Zero)));
359 case "spinningbarrelsprite":
360 int spriteCount = subElement.GetAttributeInt(
"spriteamount", 1);
361 for (
int i = 0; i < spriteCount; i++)
363 spinningBarrelSprites.Add(
new Sprite(subElement));
370 isSlowTurret =
item.
HasTag(
"slowturret".ToIdentifier());
371 InitProjSpecific(element);
376 private void UpdateTransformedBarrelPos()
380 item.ResetCachedVisibleSize();
389 if (loadedRotationLimits.HasValue) {
RotationLimits = loadedRotationLimits.Value; }
390 if (loadedBaseRotation.HasValue) {
BaseRotation = loadedBaseRotation.Value; }
392 UpdateTransformedBarrelPos();
400 connectionPanel.
Connections.RemoveAll(c => c.Name is ToggleAutoOperateConnection or SetAutoOperateConnection && c.Wires.None());
405 private void FindLightComponents()
407 if (lightComponents !=
null)
412 foreach (LightComponent lc
in item.GetComponents<LightComponent>())
417 lightComponents ??=
new List<LightComponent>();
418 lightComponents.Add(lc);
423 if (lightComponents !=
null)
425 foreach (var light
in lightComponents)
432 light.Light.PriorityMultiplier *= 10.0f;
442 if (reload > 0.0f) { reload -= deltaTime; }
443 if (!MathUtils.NearlyEqual(
item.
Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(
item.
Scale, prevScale))
445 UpdateTransformedBarrelPos();
448 if (user is { Removed:
true })
454 resetUserTimer -= deltaTime;
455 if (resetUserTimer <= 0.0f) { user =
null; }
464 resetActiveUserTimer -= deltaTime;
465 if (resetActiveUserTimer <= 0.0f)
473 float previousChargeTime = currentChargeTime;
479 currentChargeTime =
Reload > 0.0f ?
485 float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime;
486 if (chargeDeltaTime > 0f && user !=
null)
490 currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f,
MaxChargeTime);
492 tryingToCharge =
false;
494 if (currentChargeTime == 0f)
496 currentChargingState = ChargingState.Inactive;
498 else if (currentChargeTime < previousChargeTime)
500 currentChargingState = ChargingState.WindingDown;
505 currentChargingState = ChargingState.WindingUp;
508 UpdateProjSpecific(deltaTime);
510 if (MathUtils.NearlyEqual(minRotation, maxRotation))
516 float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
518 float maxDist = (maxRotation - minRotation) / 2.0f;
520 if (Math.Abs(targetMidDiff) > maxDist)
522 targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation;
526 if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; }
543 float rotMidDiff = MathHelper.WrapAngle(
Rotation - (minRotation + maxRotation) / 2.0f);
545 float targetRotationDiff = MathHelper.WrapAngle(targetRotation -
Rotation);
547 if ((maxRotation - minRotation) < MathHelper.TwoPi)
549 float targetRotationMaxDiff = MathHelper.WrapAngle(targetRotation - maxRotation);
550 float targetRotationMinDiff = MathHelper.WrapAngle(targetRotation - minRotation);
552 if (Math.Abs(targetRotationMaxDiff) < Math.Abs(targetRotationMinDiff) &&
554 targetRotationDiff < 0.0f)
556 targetRotationDiff += MathHelper.TwoPi;
558 else if (Math.Abs(targetRotationMaxDiff) > Math.Abs(targetRotationMinDiff) &&
560 targetRotationDiff > 0.0f)
562 targetRotationDiff -= MathHelper.TwoPi;
567 (targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime;
568 angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
570 Rotation += angularVelocity * deltaTime;
572 rotMidDiff = MathHelper.WrapAngle(
Rotation - (minRotation + maxRotation) / 2.0f);
574 if (rotMidDiff < -maxDist)
577 angularVelocity *= -0.5f;
579 else if (rotMidDiff > maxDist)
582 angularVelocity *= -0.5f;
585 if (aiFindTargetTimer > 0.0f)
587 aiFindTargetTimer -= deltaTime;
600 if (lightComponents !=
null)
602 foreach (var light
in lightComponents)
609 partial
void UpdateProjSpecific(
float deltaTime);
611 private bool isUseBeingCalled;
617 if (isUseBeingCalled) {
return false; }
619 isUseBeingCalled =
true;
620 bool wasSuccessful = TryLaunch(deltaTime, character);
621 isUseBeingCalled =
false;
622 return wasSuccessful;
640 private Vector2 GetBarrelDir()
645 private bool TryLaunch(
float deltaTime,
Character character =
null,
bool ignorePower =
false)
647 tryingToCharge =
true;
652 if (reload > 0.0f) {
return false; }
656 activeProjectiles.RemoveAll(it => it.Removed);
668 if (!flashLowPower && character !=
null && character ==
Character.Controlled)
670 flashLowPower =
true;
671 SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
678 Projectile launchedProjectile =
null;
679 bool loaderBroken =
false;
680 float tinkeringStrength = 0f;
684 var projectiles = GetLoadedProjectiles();
685 if (projectiles.Any())
687 ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
688 if (projectileContainer !=
null && projectileContainer.Item !=
item)
691 projectileContainer?.Item.Use(deltaTime, user:
null, userForOnUsedEvent: user);
701 if (e is not
Item linkedItem) {
continue; }
703 if (linkedItem.Condition <= 0.0f)
708 if (tryUseProjectileContainer(linkedItem)) {
break; }
710 tryUseProjectileContainer(
item);
712 bool tryUseProjectileContainer(
Item containerItem)
714 ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
715 if (projectileContainer !=
null)
717 containerItem.Use(deltaTime, user:
null, userForOnUsedEvent: user);
718 projectiles = GetLoadedProjectiles();
719 if (projectiles.Any()) {
return true; }
729 failedLaunchAttempts++;
731 if (!flashNoAmmo && !flashLoaderBroken && character !=
null && character ==
Character.Controlled && failedLaunchAttempts > 20)
735 flashLoaderBroken =
true;
741 failedLaunchAttempts = 0;
742 SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
747 failedLaunchAttempts = 0;
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))
755 tinkeringStrength = repairable.TinkeringStrength;
765 neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
766 while (neededPower > 0.0001f && batteries.Any())
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)
772 neededPower -= takePower;
773 battery.Charge -= takePower / 3600.0f;
775 battery.Item.CreateServerEvent(battery);
781 launchedProjectile = projectiles.FirstOrDefault();
782 Item container = launchedProjectile?.Item.Container;
783 if (container !=
null)
785 var repairable = launchedProjectile?.Item.Container.GetComponent<Repairable>();
786 if (repairable !=
null)
788 repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
794 if (launchedProjectile?.
Item.GetComponent<Rope>() !=
null &&
795 lastProjectile?.
Item.GetComponent<Rope>() is { SnapWhenWeaponFiredAgain:
true } rope)
800 if (projectiles.Any())
802 foreach (Projectile projectile
in projectiles)
804 Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength);
809 Launch(
null, character, tinkeringStrength: tinkeringStrength);
816 if (container !=
null)
818 ShiftItemsInProjectileContainer(container.GetComponent<ItemContainer>());
822 currentLoaderIndex = (currentLoaderIndex + 1) %
item.
linkedTo.Count;
827 lastProjectile = launchedProjectile;
830 if (character !=
null && launchedProjectile !=
null)
833 var containedItems = launchedProjectile.Item.ContainedItems;
834 if (containedItems ==
null || !containedItems.Any())
840 msg +=
", contained items: " +
string.Join(
", ", containedItems.Select(i => i.Name)) +
")";
849 private readonly
struct EventData : IEventData
851 public readonly
Item Projectile;
853 public EventData(
Item projectile,
Turret turret)
855 Projectile = projectile;
859 private void Launch(
Item projectile, Character user =
null,
float? launchRotation =
null,
float tinkeringStrength = 0f)
871 reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease);
878 if (projectile !=
null)
882 flipFiringOffset = !flipFiringOffset;
884 activeProjectiles.Add(projectile);
885 projectile.Drop(
null, setTransform:
false);
886 if (projectile.body !=
null)
888 projectile.body.Dir = 1.0f;
889 projectile.body.ResetDynamics();
890 projectile.body.Enabled =
true;
893 float spread = MathHelper.ToRadians(
Spread) * Rand.Range(-0.5f, 0.5f);
895 Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition());
899 Body pickedBody =
Submarine.PickBody(ConvertUnits.ToSimUnits(
item.
WorldPosition), launchPos,
null, Physics.CollisionWall, allowInsideFixture:
true,
900 customPredicate: (Fixture f) =>
904 if (pickedBody !=
null)
906 launchPos =
Submarine.LastPickedPosition;
908 projectile.SetTransform(launchPos, -(launchRotation ??
Rotation) + spread);
909 projectile.UpdateTransform();
910 projectile.Submarine = projectile.body?.Submarine;
912 Projectile projectileComponent = projectile.GetComponent<Projectile>();
913 if (projectileComponent !=
null)
915 TryDetermineProjectileSpeed(projectileComponent);
916 projectileComponent.Launcher =
item;
917 projectileComponent.Attacker = projectileComponent.User = user;
918 if (projectileComponent.Attack !=
null)
920 projectileComponent.Attack.DamageMultiplier = (1f *
DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
923 if (
item.GetComponent<TriggerComponent>() is { } trigger)
925 projectileComponent.IgnoredBodies.Add(trigger.PhysicsBody.FarseerBody);
927 projectile.GetComponent<Rope>()?.Attach(
item, projectile);
928 projectileComponent.User = user;
933 if (velocitySum.LengthSquared() < NetConfig.MaxPhysicsBodyVelocity * NetConfig.MaxPhysicsBodyVelocity * 0.9f)
935 projectile.body.LinearVelocity = velocitySum;
940 projectile.Container?.RemoveContained(projectile);
943 item.CreateServerEvent(
this,
new EventData(projectile,
this));
947 LaunchProjSpecific();
950 private void TryDetermineProjectileSpeed(Projectile projectile)
952 if (projectile !=
null && !projectile.Hitscan)
955 ConvertUnits.ToDisplayUnits(
956 MathHelper.Clamp((projectile.LaunchImpulse +
LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity));
960 partial
void LaunchProjSpecific();
962 private static void ShiftItemsInProjectileContainer(ItemContainer container)
964 if (container ==
null) {
return; }
969 for (
int i = 1; i < container.Capacity; i++)
971 if (container.Inventory.GetItemAt(i) is
Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1))
973 if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping:
false, allowCombine:
false, user:
null, createNetworkEvent:
true))
982 private float waitTimer;
983 private float randomAimTimer;
985 private float prevTargetRotation;
986 private float updateTimer;
987 private bool updatePending;
991 public void UpdateAutoOperate(
float deltaTime,
bool ignorePower, Identifier friendlyTag =
default)
1000 if (friendlyTag.IsEmpty)
1012 if (updateTimer < 0.0f)
1015 item.CreateServerEvent(
this);
1017 prevTargetRotation = targetRotation;
1018 updateTimer = 0.25f;
1020 updateTimer -= deltaTime;
1025 waitTimer -= deltaTime;
1029 float maxDistance = 10000.0f;
1030 float shootDistance =
AIRange;
1032 float closestDist = shootDistance * shootDistance;
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; }
1045 if (currentTarget !=
null && target == currentTarget)
1047 priority *= GetTargetPriorityModifier();
1049 closestDist = dist / priority;
1056 if (!IsValidTarget(targetItem)) {
continue; }
1058 if (priority <= 0) {
continue; }
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)
1067 priority *= GetTargetPriorityModifier();
1069 closestDist = dist / priority;
1074 if (target ==
null || target.
Submarine !=
null)
1076 closestDist = maxDistance * maxDistance;
1085 if (dist > closestDist) {
continue; }
1089 closestDist = shootDistance * shootDistance;
1090 if (closestSub !=
null)
1096 if (dist > closestDist) {
continue; }
1108 waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f);
1109 targetRotation = Rand.Range(minRotation, maxRotation);
1110 updatePending =
true;
1118 if (randomAimTimer < 0)
1122 waitTimer = Rand.Range(0.25f, 1f);
1124 targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-randomAim, randomAim));
1125 updatePending =
true;
1130 randomAimTimer -= deltaTime;
1134 if (target ==
null) {
return; }
1135 currentTarget = target;
1138 targetRotation = MathUtils.WrapAngleTwoPi(angle);
1139 if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending =
true; }
1141 if (target is
Hull targetHull)
1143 Vector2 barrelDir = GetBarrelDir();
1151 if (!IsWithinAimingRadius(angle)) {
return; }
1155 Vector2 end = ConvertUnits.ToSimUnits(target.
WorldPosition);
1157 Body worldTarget = CheckLineOfSight(start, end);
1163 Body transformedTarget = CheckLineOfSight(start, end);
1164 shoot = CanShoot(transformedTarget, user:
null, friendlyTag,
TargetSubmarines) && (worldTarget ==
null || CanShoot(worldTarget, user:
null, friendlyTag,
TargetSubmarines));
1172 TryLaunch(deltaTime, ignorePower: ignorePower);
1180 if (previousTarget.LastAttacker ==
null || previousTarget.LastAttacker == character)
1182 character.
Speak(TextManager.Get(
"DialogTurretTargetDead").Value,
1183 identifier: $
"killedtarget{previousTarget.ID}".ToIdentifier(),
1184 minDurationBetweenSimilar: 5.0f);
1192 float lowestCharge = 0.0f;
1198 if (batteryToLoad ==
null || battery.
Charge < lowestCharge)
1200 batteryToLoad = battery;
1201 lowestCharge = battery.
Charge;
1205 if (battery.
Item.
Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
1212 character.
Speak(TextManager.Get(
"DialogSupercapacitorIsBroken").Value,
1213 identifier:
"supercapacitorisbroken".ToIdentifier(),
1214 minDurationBetweenSimilar: 30.0f);
1218 if (batteryToLoad ==
null) {
return true; }
1219 if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
1224 if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0)
1226 character.
Speak(TextManager.Get(
"DialogTurretHasNoPower").Value,
1227 identifier:
"turrethasnopower".ToIdentifier(),
1228 minDurationBetweenSimilar: 30.0f);
1232 int usableProjectileCount = 0;
1233 int maxProjectileCount = 0;
1238 if (e is
Item projectileContainer)
1240 var container = projectileContainer.GetComponent<
ItemContainer>();
1241 if (container !=
null)
1243 maxProjectileCount += container.
Capacity;
1244 var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f);
1245 var firstProjectile = projectiles.FirstOrDefault();
1251 projectileSpeed =
float.PositiveInfinity;
1253 previousAmmo = firstProjectile;
1254 if (projectiles.Any())
1257 firstProjectile.GetComponent<
Projectile>() ??
1258 firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent<
Projectile>();
1259 TryDetermineProjectileSpeed(projectile);
1260 usableProjectileCount += projectiles.Count();
1266 if (usableProjectileCount == 0)
1269 Item containerItem =
null;
1272 containerItem = e as
Item;
1273 if (containerItem ==
null) {
continue; }
1274 if (!containerItem.IsInteractable(character)) {
continue; }
1277 if (container !=
null) {
break; }
1279 if (container ==
null || !container.ContainableItemIdentifiers.Any())
1281 if (character.IsOnPlayerTeam)
1283 character.Speak(TextManager.GetWithVariable(
"DialogCannotLoadTurret",
"[itemname]",
item.
Name, formatCapitals:
FormatCapitals.Yes).Value,
1284 identifier:
"cannotloadturret".ToIdentifier(),
1285 minDurationBetweenSimilar: 30.0f);
1289 if (objective.SubObjectives.None())
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)
1295 character.Speak(TextManager.GetWithVariable(
"DialogLoadTurret",
"[itemname]",
item.
Name, formatCapitals:
FormatCapitals.Yes).Value,
1296 identifier:
"loadturret".ToIdentifier(),
1297 minDurationBetweenSimilar: 30.0f);
1299 loadItemsObjective.Abandoned += CheckRemainingAmmo;
1300 loadItemsObjective.Completed += CheckRemainingAmmo;
1303 void CheckRemainingAmmo()
1305 if (!character.IsOnPlayerTeam) {
return; }
1307 Identifier ammoType = container.ContainableItemIdentifiers.FirstOrNull() ??
"ammobox".ToIdentifier();
1309 if (remainingAmmo == 0)
1311 character.Speak(TextManager.Get($
"DialogOutOf{ammoType}",
"DialogOutOfTurretAmmo").Value,
1312 identifier:
"outofammo".ToIdentifier(),
1313 minDurationBetweenSimilar: 30.0f);
1315 else if (remainingAmmo < 3)
1317 character.Speak(TextManager.Get($
"DialogLowOn{ammoType}").Value,
1318 identifier:
"outofammo".ToIdentifier(),
1319 minDurationBetweenSimilar: 30.0f);
1323 if (objective.SubObjectives.Any())
1331 Vector2? targetPos =
null;
1332 float maxDistance = 10000;
1334 float closestDistance = maxDistance * maxDistance;
1335 bool hadCurrentTarget = currentTarget !=
null;
1336 if (hadCurrentTarget)
1338 bool isValidTarget = IsValidTarget(currentTarget);
1342 if (dist > closestDistance)
1344 isValidTarget =
false;
1346 else if (currentTarget is
Item targetItem)
1348 if (!IsTargetItemCloseEnough(targetItem, dist))
1350 isValidTarget =
false;
1356 currentTarget =
null;
1357 aiFindTargetTimer = CrewAIFindTargetMinInverval;
1360 if (aiFindTargetTimer <= 0.0f)
1364 if (!IsValidTarget(enemy)) {
continue; }
1366 if (priority <= 0) {
continue; }
1367 if (character.Submarine !=
null)
1372 if (enemy.
Submarine.
TeamID == character.Submarine.TeamID) {
continue; }
1382 if (dist > closestDistance) {
continue; }
1383 if (dist < shootDistance * shootDistance)
1387 if (!IsWithinAimingRadius(enemy.
WorldPosition)) {
continue; }
1389 if (currentTarget !=
null && enemy == currentTarget)
1391 priority *= GetTargetPriorityModifier();
1394 closestEnemy = enemy;
1395 closestDistance = dist / priority;
1396 currentTarget = closestEnemy;
1400 if (!IsValidTarget(targetItem)) {
continue; }
1402 if (priority <= 0) {
continue; }
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)
1410 priority *= GetTargetPriorityModifier();
1413 closestDistance = dist / priority;
1415 closestEnemy =
null;
1416 currentTarget = targetItem;
1418 aiFindTargetTimer = currentTarget ==
null ? CrewAiFindTargetMaxInterval : CrewAIFindTargetMinInverval;
1420 else if (currentTarget !=
null)
1424 bool iceSpireSpotted =
false;
1425 Vector2 targetVelocity = Vector2.Zero;
1427 if (currentTarget is
Character targetCharacter)
1430 if (targetCharacter.Submarine !=
null && targetCharacter.CurrentHull !=
null && targetCharacter.Submarine !=
item.
Submarine && !targetCharacter.CanSeeTarget(
Item))
1432 targetPos = targetCharacter.CurrentHull.WorldPosition;
1433 if (closestDistance > maxDistance * maxDistance)
1441 float closestDistSqr = closestDistance;
1442 foreach (
Limb limb
in targetCharacter.AnimController.Limbs)
1445 if (limb.
Hidden) {
continue; }
1446 if (!IsWithinAimingRadius(limb.
WorldPosition)) {
continue; }
1448 if (distSqr < closestDistSqr)
1450 closestDistSqr = distSqr;
1451 if (limb == targetCharacter.AnimController.MainLimb)
1454 closestDistSqr *= 0.5f;
1459 if (projectileSpeed <
float.PositiveInfinity && targetPos.HasValue)
1462 float dist = MathF.Sqrt(closestDistSqr);
1463 float projectileMovementTime = dist / projectileSpeed;
1465 targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
1466 Vector2 movementAmount = targetVelocity * projectileMovementTime;
1469 movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead));
1470 Vector2 futurePosition = targetPos.Value + movementAmount;
1471 targetPos = Vector2.Lerp(targetPos.Value, futurePosition,
DegreeOfSuccess(character));
1473 if (closestDistSqr > shootDistance * shootDistance)
1475 aiFindTargetTimer = CrewAIFindTargetMinInverval;
1482 currentTarget =
null;
1483 closestEnemy =
null;
1491 closestDistance = shootDistance;
1495 foreach (var cell
in wall.Cells)
1497 if (!cell.DoesDamage) {
continue; }
1498 foreach (var edge
in cell.Edges)
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))
1506 Vector2 barrelDir =
new Vector2((
float)Math.Cos(
Rotation), -(
float)Math.Sin(
Rotation));
1509 closestPoint = intersection;
1510 if (!IsWithinAimingRadius(closestPoint)) {
continue; }
1522 if (dist >
AIRange + 1000) {
continue; }
1528 float minAngle = 0.5f;
1529 if (dot < minAngle && dist > 1000)
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;
1545 if (targetPos ==
null) {
return false; }
1547 objective.ForceHighestPriority =
true;
1549 debugDrawTargetPos = targetPos.Value;
1553 if (character.IsOnPlayerTeam)
1555 if (character.AIController.SelectedAiTarget ==
null && !hadCurrentTarget)
1557 if (CreatureMetrics.RecentlyEncountered.Contains(closestEnemy.
SpeciesName) || closestEnemy.
IsHuman)
1559 character.Speak(TextManager.Get(
"DialogNewTargetSpotted").Value,
1560 identifier:
"newtargetspotted".ToIdentifier(),
1561 minDurationBetweenSimilar: 30.0f);
1563 else if (CreatureMetrics.Encountered.Contains(closestEnemy.
SpeciesName))
1565 character.Speak(TextManager.GetWithVariable(
"DialogIdentifiedTargetSpotted",
"[speciesname]", closestEnemy.
DisplayName).Value,
1566 identifier:
"identifiedtargetspotted".ToIdentifier(),
1567 minDurationBetweenSimilar: 30.0f);
1571 character.Speak(TextManager.Get(
"DialogUnidentifiedTargetSpotted").Value,
1572 identifier:
"unidentifiedtargetspotted".ToIdentifier(),
1573 minDurationBetweenSimilar: 5.0f);
1576 else if (!CreatureMetrics.Encountered.Contains(closestEnemy.
SpeciesName))
1578 character.Speak(TextManager.Get(
"DialogUnidentifiedTargetSpotted").Value,
1579 identifier:
"unidentifiedtargetspotted".ToIdentifier(),
1580 minDurationBetweenSimilar: 5.0f);
1582 CreatureMetrics.AddEncounter(closestEnemy.
SpeciesName);
1584 character.AIController.SelectTarget(closestEnemy.
AiTarget);
1586 else if (iceSpireSpotted && character.IsOnPlayerTeam)
1588 character.Speak(TextManager.Get(
"DialogIceSpireSpotted").Value,
1589 identifier:
"icespirespotted".ToIdentifier(),
1590 minDurationBetweenSimilar: 60.0f);
1593 character.CursorPosition = targetPos.Value;
1594 if (character.Submarine !=
null)
1596 character.CursorPosition -= character.Submarine.Position;
1599 if (IsPointingTowards(targetPos.Value))
1601 Vector2 barrelDir = GetBarrelDir();
1604 bool allowShootingIfNothingInWay =
false;
1605 if (currentTarget !=
null)
1608 Vector2 targetEndPos = currentTarget.
WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead);
1612 allowShootingIfNothingInWay =
1613 targetVelocity.LengthSquared() > 0.001f &&
1614 MathUtils.LineSegmentsIntersect(
1615 aimStartPos, aimEndPos,
1616 targetStartPos, targetEndPos) &&
1618 Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f;
1621 Vector2 start = ConvertUnits.ToSimUnits(aimStartPos);
1622 Vector2 end = ConvertUnits.ToSimUnits(aimEndPos);
1624 Body worldTarget = CheckLineOfSight(start, end);
1625 if (closestEnemy !=
null && closestEnemy.
Submarine !=
null)
1629 Body transformedTarget = CheckLineOfSight(start, end);
1631 CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) &&
1632 (worldTarget ==
null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay));
1636 canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay);
1638 if (!canShoot) {
return false; }
1639 if (character.IsOnPlayerTeam)
1641 character.Speak(TextManager.Get(
"DialogFireTurret").Value,
1642 identifier:
"fireturret".ToIdentifier(),
1643 minDurationBetweenSimilar: 30.0f);
1645 character.SetInput(
InputType.Shoot,
true,
true);
1650 private bool IsPointingTowards(Vector2 targetPos)
1658 maxAngleError *= 2.0f;
1660 return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError;
1663 private bool IsTargetItemCloseEnough(
Item target,
float sqrDist) =>
float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
1676 if (target ==
null) {
return false; }
1677 if (target is
Character targetCharacter)
1679 if (!targetCharacter.Enabled || targetCharacter.Removed || targetCharacter.IsDead || targetCharacter.AITurretPriority <= 0)
1684 else if (target is
Item targetItem)
1686 if (targetItem.Removed || targetItem.Condition <= 0 || !targetItem.Prefab.IsAITurretTarget || targetItem.Prefab.AITurretPriority <= 0 || targetItem.IsHidden)
1690 if (targetItem.Submarine !=
null)
1694 if (targetItem.ParentInventory !=
null)
1702 private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
1704 if (!friendlyTag.IsEmpty)
1706 if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) {
return false; }
1712 bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
1719 return !target.IsOnFriendlyTeam(turretTeam) &&
TargetHumans;
1730 private bool CanShoot(Body targetBody, Character user =
null, Identifier friendlyTag =
default,
bool targetSubmarines =
true,
bool allowShootingIfNothingInWay =
false)
1732 if (targetBody ==
null)
1735 return allowShootingIfNothingInWay;
1738 if (targetBody.UserData is Character c)
1740 targetCharacter = c;
1742 else if (targetBody.UserData is Limb limb)
1744 targetCharacter = limb.character;
1746 if (targetCharacter !=
null && !targetCharacter.Removed)
1750 if (HumanAIController.IsFriendly(user, targetCharacter))
1755 else if (!IsValidTargetForAutoOperate(targetCharacter, friendlyTag))
1763 if (targetBody.UserData is ISpatialEntity e)
1765 if (e is Structure { Indestructible:
true }) {
return false; }
1766 if (!targetSubmarines && e is Submarine) {
return false; }
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; }
1782 private Body CheckLineOfSight(Vector2 start, Vector2 end)
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) =>
1788 if (f.UserData is
Item i && i.GetComponent<
Turret>() !=
null) { return false; }
1789 if (f.UserData is Hull) { return false; }
1795 private Vector2 GetRelativeFiringPosition(
bool useOffset =
true)
1797 Vector2 transformedFiringOffset = Vector2.Zero;
1801 if (flipFiringOffset) { currOffSet.X = -currOffSet.X; }
1802 transformedFiringOffset = MathUtils.RotatePoint(
new Vector2(-currOffSet.Y, -currOffSet.X) *
item.
Scale, -
Rotation);
1804 return new Vector2(
item.
WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X,
item.
WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
1807 private bool IsWithinAimingRadius(
float angle)
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;
1819 base.RemoveComponentSpecific();
1821 barrelSprite?.
Remove(); barrelSprite =
null;
1822 railSprite?.
Remove(); railSprite =
null;
1823 barrelSpriteBroken?.
Remove(); barrelSpriteBroken =
null;
1824 railSpriteBroken?.
Remove(); railSpriteBroken =
null;
1827 crosshairSprite?.
Remove(); crosshairSprite =
null;
1828 crosshairPointerSprite?.Remove(); crosshairPointerSprite =
null;
1829 moveSoundChannel?.Dispose(); moveSoundChannel =
null;
1830 WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite =
null;
1831 if (powerIndicator !=
null)
1833 powerIndicator.RectTransform.Parent =
null;
1834 powerIndicator =
null;
1839 private List<Projectile> GetLoadedProjectiles()
1841 List<Projectile> projectiles =
new List<Projectile>();
1843 CheckProjectileContainer(
item, projectiles, out
bool _);
1848 if (e is
Item projectileContainer)
1850 CheckProjectileContainer(projectileContainer, projectiles, out
bool stopSearching);
1851 if (projectiles.Any() || stopSearching) {
return projectiles; }
1857 private static void CheckProjectileContainer(
Item projectileContainer, List<Projectile> projectiles, out
bool stopSearching)
1859 stopSearching =
false;
1860 if (projectileContainer.Condition <= 0.0f) {
return; }
1862 var containedItems = projectileContainer.ContainedItems;
1863 if (containedItems ==
null) {
return; }
1865 foreach (
Item containedItem
in containedItems)
1867 var projectileComponent = containedItem.GetComponent<Projectile>();
1868 if (projectileComponent !=
null && projectileComponent.Item.body !=
null)
1870 projectiles.Add(projectileComponent);
1876 foreach (
Item subContainedItem
in containedItem.ContainedItems)
1878 projectileComponent = subContainedItem.GetComponent<Projectile>();
1879 if (projectileComponent !=
null && projectileComponent.Item.body !=
null)
1881 projectiles.Add(projectileComponent);
1886 if (containedItem.Condition > 0.0f || projectiles.Any())
1888 stopSearching =
true;
1895 public override void FlipX(
bool relativeToSub)
1897 minRotation = MathHelper.Pi - minRotation;
1898 maxRotation = MathHelper.Pi - maxRotation;
1900 var temp = minRotation;
1901 minRotation = maxRotation;
1906 while (minRotation < 0)
1908 minRotation += MathHelper.TwoPi;
1909 maxRotation += MathHelper.TwoPi;
1911 targetRotation =
Rotation = (minRotation + maxRotation) / 2;
1913 UpdateTransformedBarrelPos();
1917 public override void FlipY(
bool relativeToSub)
1921 minRotation = -minRotation;
1922 maxRotation = -maxRotation;
1924 var temp = minRotation;
1925 minRotation = maxRotation;
1928 while (minRotation < 0)
1930 minRotation += MathHelper.TwoPi;
1931 maxRotation += MathHelper.TwoPi;
1933 targetRotation =
Rotation = (minRotation + maxRotation) / 2;
1935 UpdateTransformedBarrelPos();
1942 switch (connection.
Name)
1945 if (
float.TryParse(signal.
value, NumberStyles.Float, CultureInfo.InvariantCulture, out
float newRotation))
1947 if (!MathUtils.IsValid(newRotation)) {
return; }
1948 targetRotation = MathHelper.ToRadians(newRotation);
1953 resetActiveUserTimer = 1f;
1954 resetUserTimer = 10.0f;
1957 if (signal.
value ==
"0") {
return; }
1958 item.
Use((
float)Timing.Step, user: sender);
1961 resetActiveUserTimer = 1f;
1962 resetUserTimer = 10.0f;
1967 TryLaunch((
float)Timing.Step, sender);
1970 case "toggle_light":
1971 if (lightComponents !=
null && signal.
value !=
"0")
1973 foreach (var light
in lightComponents)
1975 light.IsOn = !light.IsOn;
1981 if (lightComponents !=
null)
1983 bool shouldBeOn = signal.
value !=
"0";
1984 foreach (var light
in lightComponents)
1986 light.IsOn = shouldBeOn;
1991 case SetAutoOperateConnection:
1995 case ToggleAutoOperateConnection:
1997 if (signal.
value !=
"0")
2005 private Vector2? loadedRotationLimits;
2006 private float? loadedBaseRotation;
2009 base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
2016 base.OnItemLoaded();
2017 FindLightComponents();
2019 if (!loadedBaseRotation.HasValue)
2028 if (TryExtractEventData(extraData, out EventData eventData))
2030 msg.
WriteUInt16(eventData.Projectile?.ID ?? LaunchWithoutProjectileId);
2036 msg.
WriteRangedSingle(MathHelper.Clamp(wrapAngle(targetRotation), minRotation, maxRotation), minRotation, maxRotation, 16);
2039 float wrapAngle(
float angle)
2041 float wrappedAngle = angle;
2042 while (wrappedAngle < minRotation && MathUtils.IsValid(wrappedAngle))
2044 wrappedAngle += MathHelper.TwoPi;
2046 while (wrappedAngle > maxRotation && MathUtils.IsValid(wrappedAngle))
2048 wrappedAngle -= MathHelper.TwoPi;
2050 return wrappedAngle;
virtual void SelectTarget(AITarget target)
AITarget SelectedAiTarget
void AddSubObjective(AIObjective objective, bool addFirst=false)
readonly AIObjectiveManager objectiveManager
override bool IsValidTarget(Item item)
readonly CharacterParams Params
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
virtual AIController AIController
float GetStatValue(StatTypes statType, bool includeSaved=true)
static readonly List< Character > CharacterList
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 AISlowTurretPriority
float GetAttributeFloat(string key, float def)
Vector2 GetAttributeVector2(string key, in Vector2 def)
virtual Vector2 WorldPosition
static NetworkMember NetworkMember
static GameSession GameSession
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?
List< Repairable > Repairables
float OffsetOnSelectedMultiplier
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 HasTag(Identifier tag)
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
float ConditionPercentage
List< Fixture > StaticFixtures
float AISlowTurretPriority
readonly List< Connection > Connections
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()
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.
float SpringDampingLowSkill
CharacterTeamType FriendlyTeam
float AICurrentTargetPriorityMultiplier
float RotationSpeedHighSkill
float SpringDampingHighSkill
override void Update(float deltaTime, Camera cam)
void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData=null)
bool AlternatingFiringOffset
override void FlipX(bool relativeToSub)
float GetPowerRequiredToShoot()
void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag=default)
float SpringStiffnessHighSkill
float FiringRotationSpeedModifier
bool IsWithinAimingRadius(Vector2 target)
float RetractionDurationMultiplier
override void RemoveComponentSpecific()
Turret(Item item, ContentXElement element)
void UpdateLightComponents()
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
Vector2 TransformedBarrelPos
override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
override void FlipY(bool relativeToSub)
override void ReceiveSignal(Signal signal, Connection connection)
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
float RotationSpeedLowSkill
IEnumerable< Item > ActiveProjectiles
bool LaunchWithoutProjectile
bool AllowAutoOperateWithWiring
float SpringStiffnessLowSkill
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...
List< LevelWall > ExtraWalls
Special wall chunks that aren't part of the normal level geometry: includes things like the ocean flo...
readonly MapEntityPrefab Prefab
readonly List< MapEntity > linkedTo
bool IsLinkAllowed(MapEntityPrefab target)
static string CharacterLogName(Character character)
static void Log(string line, ServerLog.MessageType messageType)
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)
override Vector2? WorldPosition
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static List< Submarine > Loaded
override Vector2 SimPosition
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)
void WriteUInt16(UInt16 val)
ActionType
ActionTypes define when a StatusEffect is executed.
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
@ Character
Characters only