Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs
2024-06-18 16:50:02 +03:00

297 lines
12 KiB
C#

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;
/// <summary>
/// Latest signal the set_force connection received, used to set <see cref="targetForce"/> 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.
/// </summary>
private float? lastReceivedTargetForce;
/// <summary>
/// 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)
/// </summary>
private float targetForce;
/// <summary>
/// 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.
/// </summary>
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 = Voltage > MinVoltage;
}
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("helm") / 100));
}
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() 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
}
}
/// <summary>
/// Power consumption of the engine. Only consume power when active and adjust consumption based on condition and target force.
/// </summary>
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<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
return currPowerConsumption;
}
/// <summary>
/// When grid is resolved update the previous voltage
/// </summary>
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;
}
}
}