Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs
Juan Pablo Arce 3ca584f2fc v0.19.8.0
2022-09-28 21:30:52 -03:00

1746 lines
73 KiB
C#

using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Barotrauma.Extensions;
using FarseerPhysics.Dynamics;
using System.Collections.Immutable;
namespace Barotrauma.Items.Components
{
partial class Turret : Powered, IDrawableComponent, IServerSerializable
{
private Sprite barrelSprite, railSprite;
private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>();
private readonly List<Sprite> spinningBarrelSprites = new List<Sprite>();
private Vector2 barrelPos;
private Vector2 transformedBarrelPos;
private float rotation, targetRotation;
private float reload, reloadTime, delayBetweenBurst;
private int shotsPerBurst, shotCounter;
private float minRotation, maxRotation;
private float launchImpulse;
private float damageMultiplier;
private Camera cam;
private float angularVelocity;
private int failedLaunchAttempts;
private float currentChargeTime;
private bool tryingToCharge;
private enum ChargingState
{
Inactive,
WindingUp,
WindingDown,
}
private ChargingState currentChargingState;
private readonly List<Item> activeProjectiles = new List<Item>();
public IEnumerable<Item> ActiveProjectiles => activeProjectiles;
private Character user;
private float resetUserTimer;
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private Character currentTarget;
const float aiFindTargetInterval = 5.0f;
private int currentLoaderIndex;
private const float TinkeringPowerCostReduction = 0.2f;
private const float TinkeringDamageIncrease = 0.2f;
private const float TinkeringReloadDecrease = 0.2f;
public Character ActiveUser;
private float resetActiveUserTimer;
private List<LightComponent> lightComponents;
public float Rotation
{
get { return rotation; }
}
[Serialize("0,0", IsPropertySaveable.No, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")]
public Vector2 BarrelPos
{
get
{
return barrelPos;
}
set
{
barrelPos = value;
UpdateTransformedBarrelPos();
}
}
[Serialize("0,0", IsPropertySaveable.No, description: "The projectile launching location relative to transformed barrel position (in pixels).")]
public Vector2 FiringOffset
{
get;
set;
}
public bool flipFiringOffset;
[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.)")]
public bool AlternatingFiringOffset
{
get;
set;
}
public Vector2 TransformedBarrelPos
{
get
{
return transformedBarrelPos;
}
}
[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).")]
public float LaunchImpulse
{
get { return launchImpulse; }
set { launchImpulse = value; }
}
[Editable(0.0f, 1000.0f, decimals: 3), Serialize(5.0f, IsPropertySaveable.No, description: "The period of time the user has to wait between shots.")]
public float Reload
{
get { return reloadTime; }
set { reloadTime = value; }
}
[Editable(1, 100), Serialize(1, IsPropertySaveable.No, description: "How many projectiles needs to be shot before we add an extra break? Think of the double coilgun.")]
public int ShotsPerBurst
{
get { return shotsPerBurst; }
set { shotsPerBurst = value; }
}
[Editable(0.0f, 1000.0f, decimals: 3), Serialize(0.0f, IsPropertySaveable.No, description: "An extra delay between the bursts. Added to the reload.")]
public float DelayBetweenBursts
{
get { return delayBetweenBurst; }
set { delayBetweenBurst = value; }
}
[Editable(0.1f, 10f), 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.")]
public float RetractionDurationMultiplier
{
get;
set;
}
[Editable(0.1f, 10f), Serialize(0.1f, IsPropertySaveable.No, description: "How quickly the recoil moves the barrel after launching.")]
public float RecoilTime
{
get;
set;
}
[Editable(0f, 1000f), Serialize(0f, IsPropertySaveable.No, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")]
public float RetractionDelay
{
get;
set;
}
[Serialize(1.0f, IsPropertySaveable.No, description: "Multiplies the damage the turret deals by this amount.")]
public float DamageMultiplier
{
get { return damageMultiplier; }
set { damageMultiplier = value; }
}
[Serialize(1, IsPropertySaveable.No, description: "How many projectiles the weapon launches when fired once.")]
public int ProjectileCount
{
get;
set;
}
[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).")]
public bool LaunchWithoutProjectile
{
get;
set;
}
[Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }),
Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)]
public Vector2 RotationLimits
{
get
{
return new Vector2(MathHelper.ToDegrees(minRotation), MathHelper.ToDegrees(maxRotation));
}
set
{
minRotation = MathHelper.ToRadians(Math.Min(value.X, value.Y));
maxRotation = MathHelper.ToRadians(Math.Max(value.X, value.Y));
rotation = (minRotation + maxRotation) / 2;
#if CLIENT
if (lightComponents != null)
{
foreach (var light in lightComponents)
{
light.Rotation = rotation;
light.Light.Rotation = -rotation;
}
}
#endif
}
}
[Serialize(0.0f, IsPropertySaveable.No, description: "Random spread applied to the firing angle of the projectiles (in degrees).")]
public float Spread
{
get;
set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
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.")]
public float SpringStiffnessLowSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
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.")]
public float SpringStiffnessHighSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
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.")]
public float SpringDampingLowSkill
{
get;
private set;
}
[Editable(0.0f, 1000.0f, DecimalCount = 2),
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.")]
public float SpringDampingHighSkill
{
get;
private set;
}
[Editable(0.0f, 100.0f, DecimalCount = 2),
Serialize(1.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with insufficient skills to operate it.")]
public float RotationSpeedLowSkill
{
get;
private set;
}
[Editable(0.0f, 100.0f, DecimalCount = 2),
Serialize(5.0f, IsPropertySaveable.No, description: "Maximum angular velocity of the barrel when used by a character with sufficient skills to operate it."),]
public float RotationSpeedHighSkill
{
get;
private set;
}
[Serialize(1.0f, IsPropertySaveable.No, description: "How fast the turret can rotate while firing (for charged weapons).")]
public float FiringRotationSpeedModifier
{
get;
private set;
}
[Serialize(false, IsPropertySaveable.Yes, description: "Whether the turret should always charge-up fully to shoot.")]
public bool SingleChargedShot
{
get;
private set;
}
private float prevScale;
float prevBaseRotation;
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
public float BaseRotation
{
get { return item.Rotation; }
set
{
item.Rotation = value;
UpdateTransformedBarrelPos();
}
}
[Serialize(3000.0f, IsPropertySaveable.Yes, description: "How close to a target the turret has to be for an AI character to fire it.")]
public float AIRange
{
get;
set;
}
[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.")]
public int MaxActiveProjectiles
{
get;
set;
}
[Serialize(0f, IsPropertySaveable.Yes, description: "The time required for a charge-type turret to charge up before able to fire.")]
public float MaxChargeTime
{
get;
private set;
}
public Turret(Item item, ContentXElement element)
: base(item, element)
{
IsActive = true;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "barrelsprite":
barrelSprite = new Sprite(subElement);
break;
case "railsprite":
railSprite = new Sprite(subElement);
break;
case "chargesprite":
chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero)));
break;
case "spinningbarrelsprite":
int spriteCount = subElement.GetAttributeInt("spriteamount", 1);
for (int i = 0; i < spriteCount; i++)
{
spinningBarrelSprites.Add(new Sprite(subElement));
}
break;
}
}
item.IsShootable = true;
item.RequireAimToUse = false;
InitProjSpecific(element);
}
partial void InitProjSpecific(ContentXElement element);
private void UpdateTransformedBarrelPos()
{
transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), MathHelper.ToRadians(item.Rotation));
#if CLIENT
item.ResetCachedVisibleSize();
#endif
prevBaseRotation = item.Rotation;
prevScale = item.Scale;
}
public override void OnMapLoaded()
{
base.OnMapLoaded();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
targetRotation = rotation;
UpdateTransformedBarrelPos();
}
private void FindLightComponents()
{
if (lightComponents != null)
{
// Can't run again, because of reparenting.
return;
}
foreach (LightComponent lc in item.GetComponents<LightComponent>())
{
// 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.
if (lc?.Parent == this)
{
if (lightComponents == null)
{
lightComponents = new List<LightComponent>();
}
lightComponents.Add(lc);
}
}
#if CLIENT
if (lightComponents != null)
{
foreach (var light in lightComponents)
{
// 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)
light.Parent = null;
light.Rotation = Rotation - item.RotationRad;
light.Light.Rotation = -rotation;
}
}
#endif
}
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
if (reload > 0.0f) { reload -= deltaTime; }
if (!MathUtils.NearlyEqual(item.Rotation, prevBaseRotation) || !MathUtils.NearlyEqual(item.Scale, prevScale))
{
UpdateTransformedBarrelPos();
}
if (user is { Removed: true })
{
user = null;
}
else
{
resetUserTimer -= deltaTime;
if (resetUserTimer <= 0.0f) { user = null; }
}
if (ActiveUser is { Removed: true })
{
ActiveUser = null;
}
else
{
resetActiveUserTimer -= deltaTime;
if (resetActiveUserTimer <= 0.0f)
{
ActiveUser = null;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
float previousChargeTime = currentChargeTime;
if (SingleChargedShot && reload > 0f)
{
// single charged shot guns will decharge after firing
// for cosmetic reasons, this is done by lerping in half the reload time
currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f));
}
else
{
float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime;
if (chargeDeltaTime > 0f && user != null)
{
chargeDeltaTime *= 1f + user.GetStatValue(StatTypes.TurretChargeSpeed);
}
currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
}
tryingToCharge = false;
if (currentChargeTime == 0f)
{
currentChargingState = ChargingState.Inactive;
}
else if (currentChargeTime < previousChargeTime)
{
currentChargingState = ChargingState.WindingDown;
}
else
{
// if we are charging up or at maxed charge, remain winding up
currentChargingState = ChargingState.WindingUp;
}
UpdateProjSpecific(deltaTime);
if (MathUtils.NearlyEqual(minRotation, maxRotation))
{
UpdateLightComponents();
return;
}
float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f);
float maxDist = (maxRotation - minRotation) / 2.0f;
if (Math.Abs(targetMidDiff) > maxDist)
{
targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation;
}
float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user);
if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels
float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess);
float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess);
float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess);
if (MaxChargeTime > 0)
{
rotationSpeed *= MathHelper.Lerp(1f, FiringRotationSpeedModifier, MathUtils.EaseIn(currentChargeTime / MaxChargeTime));
}
// Do not increase the weapons skill when operating a turret in an outpost level
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
{
user.Info.IncreaseSkillLevel("weapons".ToIdentifier(),
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
}
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
float targetRotationDiff = MathHelper.WrapAngle(targetRotation - rotation);
if ((maxRotation - minRotation) < MathHelper.TwoPi)
{
float targetRotationMaxDiff = MathHelper.WrapAngle(targetRotation - maxRotation);
float targetRotationMinDiff = MathHelper.WrapAngle(targetRotation - minRotation);
if (Math.Abs(targetRotationMaxDiff) < Math.Abs(targetRotationMinDiff) &&
rotMidDiff < 0.0f &&
targetRotationDiff < 0.0f)
{
targetRotationDiff += MathHelper.TwoPi;
}
else if (Math.Abs(targetRotationMaxDiff) > Math.Abs(targetRotationMinDiff) &&
rotMidDiff > 0.0f &&
targetRotationDiff > 0.0f)
{
targetRotationDiff -= MathHelper.TwoPi;
}
}
angularVelocity +=
(targetRotationDiff * springStiffness - angularVelocity * springDamping) * deltaTime;
angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed);
rotation += angularVelocity * deltaTime;
rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
if (rotMidDiff < -maxDist)
{
rotation = minRotation;
angularVelocity *= -0.5f;
}
else if (rotMidDiff > maxDist)
{
rotation = maxRotation;
angularVelocity *= -0.5f;
}
if (aiTargetingGraceTimer > 0f)
{
aiTargetingGraceTimer -= deltaTime;
}
if (aiFindTargetTimer > 0.0f)
{
aiFindTargetTimer -= deltaTime;
}
UpdateLightComponents();
}
public void UpdateLightComponents()
{
if (lightComponents != null)
{
foreach (var light in lightComponents)
{
light.Rotation = Rotation - item.RotationRad;
}
}
}
partial void UpdateProjSpecific(float deltaTime);
public override bool Use(float deltaTime, Character character = null)
{
if (!characterUsable && character != null) { return false; }
return TryLaunch(deltaTime, character);
}
public float GetPowerRequiredToShoot()
{
float powerCost = powerConsumption;
if (user != null)
{
powerCost /= (1 + user.GetStatValue(StatTypes.TurretPowerCostReduction));
}
return powerCost;
}
public bool HasPowerToShoot()
{
return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot();
}
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
{
tryingToCharge = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (currentChargeTime < MaxChargeTime) { return false; }
if (reload > 0.0f) { return false; }
if (MaxActiveProjectiles >= 0)
{
activeProjectiles.RemoveAll(it => it.Removed);
if (activeProjectiles.Count >= MaxActiveProjectiles)
{
return false;
}
}
if (!ignorePower)
{
if (!HasPowerToShoot())
{
#if CLIENT
if (!flashLowPower && character != null && character == Character.Controlled)
{
flashLowPower = true;
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
}
Projectile launchedProjectile = null;
bool loaderBroken = false;
float tinkeringStrength = 0f;
for (int i = 0; i < ProjectileCount; i++)
{
var projectiles = GetLoadedProjectiles();
if (projectiles.Any())
{
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime, null);
}
}
else
{
for (int j = 0; j < item.linkedTo.Count; j++)
{
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.Condition <= 0.0f)
{
loaderBroken = true;
continue;
}
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
linkedItem.Use(deltaTime, null);
projectiles = GetLoadedProjectiles();
if (projectiles.Any()) { break; }
}
}
}
if (projectiles.Count == 0 && !LaunchWithoutProjectile)
{
//coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched,
//causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay)
// -> attempt to launch the gun multiple times before showing the "no ammo" flash
failedLaunchAttempts++;
#if CLIENT
if (!flashNoAmmo && !flashLoaderBroken && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
{
if (loaderBroken)
{
flashLoaderBroken = true;
}
else
{
flashNoAmmo = true;
}
failedLaunchAttempts = 0;
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
failedLaunchAttempts = 0;
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item linkedItem)) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
{
tinkeringStrength = repairable.TinkeringStrength;
}
}
if (!ignorePower)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float neededPower = GetPowerRequiredToShoot();
// tinkering is currently not factored into the common method as it is checked only when shooting
// but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later
neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
while (neededPower > 0.0001f && batteries.Count > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
if (!batteries.Any()) { break; }
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
battery.Item.CreateServerEvent(battery);
#endif
}
}
}
launchedProjectile = projectiles.FirstOrDefault();
Item container = launchedProjectile?.Item.Container;
if (container != null)
{
var repairable = launchedProjectile?.Item.Container.GetComponent<Repairable>();
if (repairable != null)
{
repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f;
}
}
if (launchedProjectile != null || LaunchWithoutProjectile)
{
if (projectiles.Any())
{
foreach (Projectile projectile in projectiles)
{
Launch(projectile.Item, character, tinkeringStrength: tinkeringStrength);
}
}
else
{
Launch(null, character, tinkeringStrength: tinkeringStrength);
}
if (item.AiTarget != null)
{
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
// Turrets also have a light component, which handles the sight range.
}
if (container != null)
{
ShiftItemsInProjectileContainer(container.GetComponent<ItemContainer>());
}
if (item.linkedTo.Count > 0)
{
currentLoaderIndex = (currentLoaderIndex + 1) % item.linkedTo.Count;
}
}
}
#if SERVER
if (character != null && launchedProjectile != null)
{
string msg = GameServer.CharacterLogName(character) + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name;
var containedItems = launchedProjectile.Item.ContainedItems;
if (containedItems == null || !containedItems.Any())
{
msg += ")";
}
else
{
msg += ", contained items: " + string.Join(", ", containedItems.Select(i => i.Name)) + ")";
}
GameServer.Log(msg, ServerLog.MessageType.ItemInteraction);
}
#endif
return true;
}
private readonly struct EventData : IEventData
{
public readonly Item Projectile;
public EventData(Item projectile)
{
Projectile = projectile;
}
}
private void Launch(Item projectile, Character user = null, float? launchRotation = null, float tinkeringStrength = 0f)
{
reload = reloadTime;
if (ShotsPerBurst > 1)
{
shotCounter++;
if (shotCounter >= ShotsPerBurst)
{
reload += DelayBetweenBursts;
shotCounter = 0;
}
}
reload /= 1f + (tinkeringStrength * TinkeringReloadDecrease);
if (user != null)
{
reload /= 1 + user.GetStatValue(StatTypes.TurretAttackSpeed);
}
if (projectile != null)
{
if (AlternatingFiringOffset)
{
flipFiringOffset = !flipFiringOffset;
}
activeProjectiles.Add(projectile);
projectile.Drop(null, setTransform: false);
if (projectile.body != null)
{
projectile.body.Dir = 1.0f;
projectile.body.ResetDynamics();
projectile.body.Enabled = true;
}
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
projectile.SetTransform(
ConvertUnits.ToSimUnits(GetRelativeFiringPosition()),
-(launchRotation ?? rotation) + spread);
projectile.UpdateTransform();
projectile.Submarine = projectile.body?.Submarine;
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectileComponent.Launcher = item;
projectileComponent.Attacker = projectileComponent.User = user;
if (projectileComponent.Attack != null)
{
projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
}
projectileComponent.Use(null, LaunchImpulse);
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
if (item.Submarine != null && projectile.body != null)
{
Vector2 velocitySum = item.Submarine.PhysicsBody.LinearVelocity + projectile.body.LinearVelocity;
if (velocitySum.LengthSquared() < NetConfig.MaxPhysicsBodyVelocity * NetConfig.MaxPhysicsBodyVelocity * 0.9f)
{
projectile.body.LinearVelocity = velocitySum;
}
}
}
projectile.Container?.RemoveContained(projectile);
}
#if SERVER
item.CreateServerEvent(this, new EventData(projectile));
#endif
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
LaunchProjSpecific();
}
partial void LaunchProjSpecific();
private void ShiftItemsInProjectileContainer(ItemContainer container)
{
if (container == null) { return; }
bool moved;
do
{
moved = false;
for (int i = 1; i < container.Capacity; i++)
{
if (container.Inventory.GetItemAt(i) is Item item1 && container.Inventory.CanBePutInSlot(item1, i - 1))
{
if (container.Inventory.TryPutItem(item1, i - 1, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: true))
{
moved = true;
}
}
}
} while (moved);
}
private float waitTimer;
private float disorderTimer;
private float prevTargetRotation;
private float updateTimer;
private bool updatePending;
public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay)
{
if (ai == null) { return; }
IsActive = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (updatePending)
{
if (updateTimer < 0.0f)
{
#if SERVER
item.CreateServerEvent(this);
#endif
prevTargetRotation = targetRotation;
updateTimer = 0.25f;
}
updateTimer -= deltaTime;
}
if (!ignoreDelay && waitTimer > 0)
{
waitTimer -= deltaTime;
return;
}
Submarine closestSub = null;
float maxDistance = 10000.0f;
float shootDistance = AIRange;
ISpatialEntity target = null;
float closestDist = shootDistance * shootDistance;
if (targetHumans || targetOtherCreatures)
{
foreach (var character in Character.CharacterList)
{
if (character == null || character.Removed || character.IsDead) { continue; }
if (character.Params.Group == ai.Config.Entity) { continue; }
bool isHuman = character.IsHuman || character.Params.Group == CharacterPrefab.HumanSpeciesName;
if (isHuman)
{
if (!targetHumans)
{
// Don't target humans if not defined to.
continue;
}
}
else if (!targetOtherCreatures)
{
// Don't target other creatures if not defined to.
continue;
}
float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
target = character;
closestDist = dist;
}
}
if (targetSubmarines)
{
if (target == null || target.Submarine != null)
{
closestDist = maxDistance * maxDistance;
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player) { continue; }
float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
closestSub = sub;
closestDist = dist;
}
closestDist = shootDistance * shootDistance;
if (closestSub != null)
{
foreach (var hull in Hull.HullList)
{
if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; }
float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
target = hull;
closestDist = dist;
}
}
}
}
if (!ignoreDelay)
{
if (target == null)
{
// Random movement
waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f);
targetRotation = Rand.Range(minRotation, maxRotation);
updatePending = true;
return;
}
if (disorderTimer < 0)
{
// Random disorder
disorderTimer = Rand.Range(0f, 3f);
waitTimer = Rand.Range(0.25f, 1f);
targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f));
updatePending = true;
return;
}
else
{
disorderTimer -= deltaTime;
}
}
if (target == null) { return; }
float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
targetRotation = MathUtils.WrapAngleTwoPi(angle);
if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; }
if (target is Hull targetHull)
{
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
}
}
else
{
if (!CheckTurretAngle(angle)) { return; }
float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
}
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
bool shoot;
if (target.Submarine != null)
{
start -= target.Submarine.SimPosition;
end -= target.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, user: null, ai, targetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, ai, targetSubmarines));
}
else
{
shoot = CanShoot(worldTarget, user: null, ai, targetSubmarines);
}
if (shoot)
{
TryLaunch(deltaTime, ignorePower: true);
}
}
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && previousTarget.IsDead)
{
character.Speak(TextManager.Get("DialogTurretTargetDead").Value,
identifier: $"killedtarget{previousTarget.ID}".ToIdentifier(),
minDurationBetweenSimilar: 10.0f);
character.AIController.SelectTarget(null);
}
bool canShoot = true;
if (!HasPowerToShoot())
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float lowestCharge = 0.0f;
PowerContainer batteryToLoad = null;
foreach (PowerContainer battery in batteries)
{
if (!battery.Item.IsInteractable(character)) { continue; }
if (batteryToLoad == null || battery.Charge < lowestCharge)
{
batteryToLoad = battery;
lowestCharge = battery.Charge;
}
if (battery.Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(battery.Item, character))
{
if (battery.Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
{
objective.AddSubObjective(new AIObjectiveRepairItem(character, battery.Item, objective.objectiveManager, isPriority: true));
return false;
}
else
{
character.Speak(TextManager.Get("DialogSupercapacitorIsBroken").Value,
identifier: "supercapacitorisbroken".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
}
if (batteryToLoad == null) { return true; }
if (batteryToLoad.RechargeSpeed < batteryToLoad.MaxRechargeSpeed * 0.4f)
{
objective.AddSubObjective(new AIObjectiveOperateItem(batteryToLoad, character, objective.objectiveManager, option: Identifier.Empty, requireEquip: false));
return false;
}
if (lowestCharge <= 0 && batteryToLoad.Item.ConditionPercentage > 0)
{
character.Speak(TextManager.Get("DialogTurretHasNoPower").Value,
identifier: "turrethasnopower".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
canShoot = false;
}
}
int usableProjectileCount = 0;
int maxProjectileCount = 0;
foreach (MapEntity e in item.linkedTo)
{
if (!item.IsInteractable(character)) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (e is Item projectileContainer)
{
var container = projectileContainer.GetComponent<ItemContainer>();
if (container != null)
{
maxProjectileCount += container.Capacity;
int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f);
usableProjectileCount += projectiles;
}
}
}
if (usableProjectileCount == 0)
{
ItemContainer container = null;
Item containerItem = null;
foreach (MapEntity e in item.linkedTo)
{
containerItem = e as Item;
if (containerItem == null) { continue; }
if (!containerItem.IsInteractable(character)) { continue; }
if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; }
container = containerItem.GetComponent<ItemContainer>();
if (container != null) { break; }
}
if (container == null || !container.ContainableItemIdentifiers.Any())
{
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value,
identifier: "cannotloadturret".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
return true;
}
if (objective.SubObjectives.None())
{
var loadItemsObjective = AIContainItems<Turret>(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true, dropItemOnDeselected: true);
loadItemsObjective.ignoredContainerIdentifiers = ((MapEntity)containerItem).Prefab.Identifier.ToEnumerable().ToImmutableHashSet();
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, formatCapitals: FormatCapitals.Yes).Value,
identifier: "loadturret".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
loadItemsObjective.Abandoned += CheckRemainingAmmo;
loadItemsObjective.Completed += CheckRemainingAmmo;
return false;
void CheckRemainingAmmo()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
Identifier ammoType = container.ContainableItemIdentifiers.FirstOrNull() ?? "ammobox".ToIdentifier();
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
if (remainingAmmo == 0)
{
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", "DialogOutOfTurretAmmo").Value,
identifier: "outofammo".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
else if (remainingAmmo < 3)
{
character.Speak(TextManager.Get($"DialogLowOn{ammoType}").Value,
identifier: "outofammo".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
}
}
if (objective.SubObjectives.Any())
{
return false;
}
}
//enough shells and power
Character closestEnemy = null;
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
// use full range only if we're actively firing
if (aiTargetingGraceTimer <= 0f)
{
shootDistance *= 0.75f;
}
float closestDistance = maxDistance * maxDistance;
bool hadCurrentTarget = currentTarget != null;
if (hadCurrentTarget)
{
if (currentTarget.Removed || currentTarget.IsDead)
{
currentTarget = null;
}
}
if (aiFindTargetTimer <= 0.0f || currentTarget == null)
{
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled) { continue; }
if (character.Submarine != null)
{
if (enemy.Submarine == character.Submarine) { continue; }
if (enemy.Submarine != null && enemy.Submarine.TeamID == character.Submarine.TeamID) { continue; }
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
{
// Only check the angle to targets that are close enough to be shot at
// 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.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
}
closestEnemy = enemy;
closestDistance = dist;
}
currentTarget = closestEnemy;
aiFindTargetTimer = aiFindTargetInterval;
}
else
{
closestEnemy = currentTarget;
}
if (closestEnemy != null)
{
targetPos = closestEnemy.WorldPosition;
//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
if (closestEnemy.Submarine != null && closestEnemy.CurrentHull != null && closestEnemy.Submarine != item.Submarine && !closestEnemy.CanSeeTarget(Item))
{
targetPos = closestEnemy.CurrentHull.WorldPosition;
}
else
{
// 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.
float closestDist = closestDistance;
foreach (Limb limb in closestEnemy.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (dist < closestDist)
{
closestDist = dist;
targetPos = limb.WorldPosition;
}
}
if (closestDist > shootDistance * shootDistance)
{
// Not close enough to shoot
closestEnemy = null;
targetPos = null;
}
}
}
else if (item.Submarine != null && Level.Loaded != null)
{
// Check ice spires
shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls)
{
if (!(wall is DestructibleLevelWall destructibleWall) || destructibleWall.Destroyed) { continue; }
foreach (var cell in wall.Cells)
{
if (cell.DoesDamage)
{
foreach (var edge in cell.Edges)
{
Vector2 p1 = edge.Point1 + cell.Translation;
Vector2 p2 = edge.Point2 + cell.Translation;
Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition);
if (!CheckTurretAngle(closestPoint))
{
// The closest point can't be targeted -> get a point directly in front of the turret
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
{
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
}
else
{
continue;
}
}
float dist = Vector2.Distance(closestPoint, item.WorldPosition);
//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
closestPoint += (closestPoint - item.WorldPosition) / Math.Max(dist, 1);
if (dist > AIRange + 1000) { continue; }
float dot = 0;
if (!MathUtils.NearlyEqual(item.Submarine.Velocity, Vector2.Zero))
{
dot = Vector2.Dot(Vector2.Normalize(item.Submarine.Velocity), Vector2.Normalize(closestPoint - item.Submarine.WorldPosition));
}
float minAngle = 0.5f;
if (dot < minAngle && dist > 1000)
{
// The sub is not moving towards the target and it's not very close to the turret either -> ignore
continue;
}
// Allow targeting farther when heading towards the spire (up to 1000 px)
dist -= MathHelper.Lerp(0, 1000, MathUtils.InverseLerp(minAngle, 1, dot));
if (dist > closestDistance) { continue; }
targetPos = closestPoint;
closestDistance = dist;
}
}
}
}
}
if (targetPos == null) { return false; }
// Force the highest priority so that we don't change the objective while targeting enemies.
objective.ForceHighestPriority = true;
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
{
if (character.IsOnPlayerTeam)
{
if (character.AIController.SelectedAiTarget == null && !hadCurrentTarget)
{
if (CreatureMetrics.Instance.RecentlyEncountered.Contains(closestEnemy.SpeciesName) || closestEnemy.IsHuman)
{
character.Speak(TextManager.Get("DialogNewTargetSpotted").Value,
identifier: "newtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
else if (CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.GetWithVariable("DialogIdentifiedTargetSpotted", "[speciesname]", closestEnemy.DisplayName).Value,
identifier: "identifiedtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
else
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value,
identifier: "unidentifiedtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
}
else if (!CreatureMetrics.Instance.Encountered.Contains(closestEnemy.SpeciesName))
{
character.Speak(TextManager.Get("DialogUnidentifiedTargetSpotted").Value,
identifier: "unidentifiedtargetspotted".ToIdentifier(),
minDurationBetweenSimilar: 5.0f);
}
character.AddEncounter(closestEnemy);
}
character.AIController.SelectTarget(closestEnemy.AiTarget);
}
else if (closestEnemy == null && character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogIceSpireSpotted").Value,
identifier: "icespirespotted".ToIdentifier(),
minDurationBetweenSimilar: 60.0f);
}
character.CursorPosition = targetPos.Value;
if (character.Submarine != null)
{
character.CursorPosition -= character.Submarine.Position;
}
float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition);
float turretAngle = -rotation;
float maxAngleError = 0.15f;
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
{
//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)
maxAngleError *= 2.0f;
}
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; }
if (canShoot)
{
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
bool shoot;
if (closestEnemy != null && closestEnemy.Submarine != null)
{
start -= closestEnemy.Submarine.SimPosition;
end -= closestEnemy.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
}
else
{
shoot = CanShoot(worldTarget, character);
}
if (!shoot) { return false; }
if (character.IsOnPlayerTeam)
{
character.Speak(TextManager.Get("DialogFireTurret").Value,
identifier: "fireturret".ToIdentifier(),
minDurationBetweenSimilar: 30.0f);
}
character.SetInput(InputType.Shoot, true, true);
}
aiTargetingGraceTimer = 5f;
return false;
}
/// <summary>
/// Turret doesn't consume grid power, directly takes from the batteries on its grid instead.
/// </summary>
public override float GetCurrentPowerConsumption(Connection conn = null)
{
return 0;
}
private bool CanShoot(Body targetBody, Character user = null, WreckAI ai = null, bool targetSubmarines = true)
{
if (targetBody == null) { return false; }
Character targetCharacter = null;
if (targetBody.UserData is Character c)
{
targetCharacter = c;
}
else if (targetBody.UserData is Limb limb)
{
targetCharacter = limb.character;
}
if (targetCharacter != null)
{
if (user != null)
{
if (HumanAIController.IsFriendly(user, targetCharacter))
{
return false;
}
}
if (ai != null)
{
if (targetCharacter.Params.Group == ai.Config.Entity)
{
return false;
}
}
}
else
{
if (targetBody.UserData is ISpatialEntity e)
{
Submarine sub = e.Submarine ?? e as Submarine;
if (!targetSubmarines && e is Submarine) { return false; }
if (sub == null) { return false; }
if (sub == Item.Submarine) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
else if (!(targetBody.UserData is Voronoi2.VoronoiCell cell && cell.IsDestructible))
{
// Hit something else, probably a level wall
return false;
}
}
return true;
}
private Body CheckLineOfSight(Vector2 start, Vector2 end)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
Body pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true,
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
return !item.StaticFixtures.Contains(f);
});
return pickedBody;
}
private Vector2 GetRelativeFiringPosition(bool useOffset = true)
{
Vector2 transformedFiringOffset = Vector2.Zero;
if (useOffset)
{
Vector2 currOffSet = FiringOffset;
if (flipFiringOffset) { currOffSet.X = -currOffSet.X; }
transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-currOffSet.Y, -currOffSet.X) * item.Scale, -rotation);
}
return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
}
private bool CheckTurretAngle(float angle)
{
float midRotation = (minRotation + maxRotation) / 2.0f;
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; }
return angle >= minRotation && angle <= maxRotation;
}
private bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
barrelSprite?.Remove(); barrelSprite = null;
railSprite?.Remove(); railSprite = null;
#if CLIENT
crosshairSprite?.Remove(); crosshairSprite = null;
crosshairPointerSprite?.Remove(); crosshairPointerSprite = null;
moveSoundChannel?.Dispose(); moveSoundChannel = null;
WeaponIndicatorSprite?.Remove(); WeaponIndicatorSprite = null;
if (powerIndicator != null)
{
powerIndicator.RectTransform.Parent = null;
powerIndicator = null;
}
#endif
}
private List<Projectile> GetLoadedProjectiles()
{
List<Projectile> projectiles = new List<Projectile>();
// check the item itself first
CheckProjectileContainer(item, projectiles, out bool _);
for (int j = 0; j < item.linkedTo.Count; j++)
{
var e = item.linkedTo[(j + currentLoaderIndex) % item.linkedTo.Count];
if (!item.Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (e is Item projectileContainer)
{
CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching);
if (projectiles.Any() || stopSearching) { return projectiles; }
}
}
return projectiles;
}
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
{
stopSearching = false;
if (projectileContainer.Condition <= 0.0f) { return; }
var containedItems = projectileContainer.ContainedItems;
if (containedItems == null) { return; }
foreach (Item containedItem in containedItems)
{
var projectileComponent = containedItem.GetComponent<Projectile>();
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
return;
}
else
{
//check if the contained item is another itemcontainer with projectiles inside it
foreach (Item subContainedItem in containedItem.ContainedItems)
{
projectileComponent = subContainedItem.GetComponent<Projectile>();
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
}
}
// in the case that we found a container that still has condition/ammo left,
// return and inform GetLoadedProjectiles to stop searching past this point (even if no projectiles were not found)
if (containedItem.Condition > 0.0f || projectiles.Any())
{
stopSearching = true;
return;
}
}
}
}
public override void FlipX(bool relativeToSub)
{
minRotation = MathHelper.Pi - minRotation;
maxRotation = MathHelper.Pi - maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
barrelPos.X = item.Rect.Width / item.Scale - barrelPos.X;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
targetRotation = rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();
UpdateLightComponents();
}
public override void FlipY(bool relativeToSub)
{
BaseRotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(MathHelper.ToRadians(180 - BaseRotation)));
minRotation = -minRotation;
maxRotation = -maxRotation;
var temp = minRotation;
minRotation = maxRotation;
maxRotation = temp;
while (minRotation < 0)
{
minRotation += MathHelper.TwoPi;
maxRotation += MathHelper.TwoPi;
}
targetRotation = rotation = (minRotation + maxRotation) / 2;
UpdateTransformedBarrelPos();
UpdateLightComponents();
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
Character sender = signal.sender;
switch (connection.Name)
{
case "position_in":
if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newRotation))
{
if (!MathUtils.IsValid(newRotation)) { return; }
targetRotation = MathHelper.ToRadians(newRotation);
IsActive = true;
}
user = sender;
ActiveUser = sender;
resetActiveUserTimer = 1f;
resetUserTimer = 10.0f;
break;
case "trigger_in":
if (signal.value == "0") { return; }
item.Use((float)Timing.Step, sender);
user = sender;
ActiveUser = sender;
resetActiveUserTimer = 1f;
resetUserTimer = 10.0f;
//triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character
//so lets do it manually
if (!characterUsable && sender != null)
{
TryLaunch((float)Timing.Step, sender);
}
break;
case "toggle_light":
if (lightComponents != null && signal.value != "0")
{
foreach (var light in lightComponents)
{
light.IsOn = !light.IsOn;
}
UpdateLightComponents();
}
break;
case "set_light":
if (lightComponents != null)
{
bool shouldBeOn = signal.value != "0";
foreach (var light in lightComponents)
{
light.IsOn = shouldBeOn;
}
UpdateLightComponents();
}
break;
}
}
private Vector2? loadedRotationLimits;
private float? loadedBaseRotation;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
FindLightComponents();
targetRotation = rotation;
if (!loadedBaseRotation.HasValue)
{
if (item.FlippedX) { FlipX(relativeToSub: false); }
if (item.FlippedY) { FlipY(relativeToSub: false); }
}
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{
if (TryExtractEventData(extraData, out EventData eventData))
{
msg.WriteUInt16(eventData.Projectile.ID);
msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16);
}
else
{
msg.WriteUInt16((ushort)0);
float wrappedTargetRotation = targetRotation;
while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation))
{
wrappedTargetRotation += MathHelper.TwoPi;
}
while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation))
{
wrappedTargetRotation -= MathHelper.TwoPi;
}
msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16);
}
}
}
}