297 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|