using Microsoft.Xna.Framework; using System; using System.Globalization; using System.Xml.Linq; using Barotrauma.Networking; namespace Barotrauma.Items.Components { partial class Engine : Powered, IServerSerializable, IClientSerializable, IDeteriorateUnderStress { private float force; /// /// Latest signal the set_force connection received, used to set in the Update method. /// We use a separate variable, because otherwise specific item update orders and sending multiple signals to set_force would lead to bugs: /// targetForce could be set to 0, then a power grid might update as if the engine was off and mark the voltage of the grid as 1, /// then another item could set the targetForce to 100 and make it run without power. /// private float? lastReceivedTargetForce; /// /// The amount of force the engine is aiming for (the actual force may be less than this, /// depending on the amount of power, the condition of the engine or boosts from talents) /// private float targetForce; /// /// Power demand of a marine engine is proportional with the cube of the square root of the thrusting force. /// In practice meaning lower thrust is more effective at conserving power than it would be if the relationship between thrust and power consumption was linear. /// Reverse exponent defined for use with overvoltage calculation: Supplying 2x power will result in 59% more force, 26% more speed, therefore 2x power. /// private const float ForceToPowerExponent = 3f / 2f; private const float PowerToForceExponent = 1.0f / ForceToPowerExponent; private float maxForce; private readonly Attack propellerDamage; private float damageTimer; private bool hasPower; private float prevVoltage; private float controlLockTimer; public Character User; [Editable(0.0f, 10000000.0f), Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { get => maxForce; set => maxForce = Math.Max(0.0f, value); } [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, description: "The position of the propeller as an offset from the item's center (in pixels)."+ " Determines where the particles spawn and the position that causes characters to take damage from the engine if the PropellerDamage is defined.")] public Vector2 PropellerPos { get; set; } [Editable, Serialize(false, IsPropertySaveable.Yes)] public bool DisablePropellerDamage { get; set; } public float Force { get { return force;} set { force = MathHelper.Clamp(value, -100.0f, 100.0f); } } public float CurrentVolume => CurrentStress; public float CurrentBrokenVolume { get { if (item.ConditionPercentage > 10.0f) { return 0.0f; } return Math.Abs(targetForce / 100.0f) * (1.0f - item.ConditionPercentage / 10.0f); } } public float CurrentStress => Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); private const float TinkeringForceIncrease = 1.5f; public Engine(Item item, ContentXElement element) : base(item, element) { IsActive = true; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "propellerdamage": propellerDamage = new Attack(subElement, item.Name + ", Engine"); break; } } InitProjSpecific(element); } partial void InitProjSpecific(ContentXElement element); public override void Update(float deltaTime, Camera cam) { UpdateOnActiveEffects(deltaTime); UpdateAnimation(deltaTime); controlLockTimer -= deltaTime; if (powerConsumption == 0.0f) { prevVoltage = 1; hasPower = true; } else { hasPower = HasPower; } if (lastReceivedTargetForce.HasValue) { targetForce = lastReceivedTargetForce.Value; } Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f); if (Math.Abs(Force) > 1.0f) { float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor); float currForce = force * MathF.Pow(voltageFactor, PowerToForceExponent); float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition; // Broken engine makes more noise. float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition); UpdateAITargets(noise); //arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines float forceMultiplier = 0.1f; if (User != null) { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel(Tags.HelmSkill) / 100)); } currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce); //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } Vector2 forceVector = new Vector2(currForce, 0); item.Submarine.ApplyForce(forceVector * deltaTime * Timing.FixedUpdateRate); UpdatePropellerDamage(deltaTime); #if CLIENT float particleInterval = 1.0f / particlesPerSec; particleTimer += deltaTime; while (particleTimer > particleInterval) { Vector2 particleVel = -forceVector.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos * item.Scale, particleVel * Rand.Range(0.8f, 1.1f), 0.0f, item.CurrentHull); particleTimer -= particleInterval; } #endif } } /// /// Power consumption of the engine. Only consume power when active and adjust consumption based on condition and target force. /// public override float GetCurrentPowerConsumption(Connection connection = null) { if (connection != this.powerIn || !IsActive) { return 0; } currPowerConsumption = MathF.Pow(Math.Abs(targetForce) / 100.0f, ForceToPowerExponent) * powerConsumption; //engines consume more power when in a bad condition item.GetComponent()?.AdjustPowerConsumption(ref currPowerConsumption); return currPowerConsumption; } /// /// When grid is resolved update the previous voltage /// public override void GridResolved(Connection connection) { if (connection == powerIn) { prevVoltage = Voltage; } } private void UpdateAITargets(float noise) { if (item.AiTarget != null) { item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, noise / 100); if (item.CurrentHull != null && item.CurrentHull.AiTarget != null) { // It's possible that some other item increases the hull's soundrange more than the engine. item.CurrentHull.AiTarget.SoundRange = Math.Max(item.CurrentHull.AiTarget.SoundRange, item.AiTarget.SoundRange); } } } private void UpdatePropellerDamage(float deltaTime) { if (DisablePropellerDamage) { return; } damageTimer += deltaTime; if (damageTimer < 0.5f) { return; } damageTimer = 0.1f; if (propellerDamage == null) { return; } float scaledDamageRange = propellerDamage.DamageRange * item.Scale; Vector2 propellerWorldPos = item.WorldPosition + PropellerPos * item.Scale; float broadRange = Math.Max(scaledDamageRange * 2, 500); foreach (Character character in Character.CharacterList) { if (!character.Enabled || character.Removed) { continue; } if (Math.Abs(character.WorldPosition.X - propellerWorldPos.X) > broadRange) { continue; } if (Math.Abs(character.WorldPosition.Y - propellerWorldPos.Y) > broadRange) { continue; } foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered || !limb.body.Enabled) { continue; } float distSqr = Vector2.DistanceSquared(limb.WorldPosition, propellerWorldPos); if (distSqr > scaledDamageRange * scaledDamageRange) { continue; } character.LastDamageSource = item; propellerDamage.DoDamage(null, character, propellerWorldPos, 1.0f, true); break; } } } partial void UpdateAnimation(float deltaTime); public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); force = MathHelper.Lerp(force, 0.0f, 0.1f); } public override void FlipX(bool relativeToSub) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); } public override void FlipY(bool relativeToSub) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); } public override void ReceiveSignal(Signal signal, Connection connection) { base.ReceiveSignal(signal, connection); if (connection.Name == "set_force") { if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce)) { controlLockTimer = 0.1f; lastReceivedTargetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f); User = signal.sender; } } } public override XElement Save(XElement parentElement) { Vector2 prevPropellerPos = PropellerPos; //undo flipping before saving if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); } if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); } XElement element = base.Save(parentElement); PropellerPos = prevPropellerPos; return element; } } }