882 lines
37 KiB
C#
882 lines
37 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Networking;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Reactor : Powered, IServerSerializable, IClientSerializable
|
|
{
|
|
const float NetworkUpdateIntervalHigh = 0.5f;
|
|
|
|
const float TemperatureBoostAmount = 25;
|
|
|
|
//the rate at which the reactor is being run on (higher rate -> higher temperature)
|
|
private float fissionRate;
|
|
|
|
//how much of the generated steam is used to spin the turbines and generate power
|
|
private float turbineOutput;
|
|
|
|
private float temperature;
|
|
|
|
//is automatic temperature control on
|
|
//(adjusts the fission rate and turbine output automatically to keep the
|
|
//amount of power generated balanced with the load)
|
|
private bool autoTemp;
|
|
|
|
private float fuelConsumptionRate;
|
|
|
|
private float meltDownTimer, meltDownDelay;
|
|
private float fireTimer, fireDelay;
|
|
|
|
private float maxPowerOutput;
|
|
private float minUpdatePowerOut;
|
|
private float maxUpdatePowerOut;
|
|
|
|
private bool unsentChanges;
|
|
private float sendUpdateTimer;
|
|
|
|
private float degreeOfSuccess;
|
|
|
|
private Vector2 optimalTemperature, allowedTemperature;
|
|
private Vector2 optimalFissionRate, allowedFissionRate;
|
|
private Vector2 optimalTurbineOutput, allowedTurbineOutput;
|
|
|
|
private float? signalControlledTargetFissionRate, signalControlledTargetTurbineOutput;
|
|
private double lastReceivedFissionRateSignalTime, lastReceivedTurbineOutputSignalTime;
|
|
|
|
private float temperatureBoost;
|
|
|
|
public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
|
|
|
|
private bool _powerOn;
|
|
|
|
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
|
|
public bool PowerOn
|
|
{
|
|
get { return _powerOn; }
|
|
set
|
|
{
|
|
_powerOn = value;
|
|
#if CLIENT
|
|
UpdateUIElementStates();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
protected override PowerPriority Priority { get { return PowerPriority.Reactor; } }
|
|
|
|
public Character LastAIUser { get; private set; }
|
|
|
|
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
|
|
public bool LastUserWasPlayer { get; private set; }
|
|
|
|
private Character lastUser;
|
|
public Character LastUser
|
|
{
|
|
get { return lastUser; }
|
|
private set
|
|
{
|
|
if (lastUser == value) { return; }
|
|
lastUser = value;
|
|
if (lastUser == null)
|
|
{
|
|
degreeOfSuccess = 0.0f;
|
|
LastUserWasPlayer = false;
|
|
}
|
|
else
|
|
{
|
|
degreeOfSuccess = Math.Min(DegreeOfSuccess(lastUser), 1.0f);
|
|
LastUserWasPlayer = lastUser.IsPlayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)]
|
|
public float MaxPowerOutput
|
|
{
|
|
get => maxPowerOutput;
|
|
set => maxPowerOutput = Math.Max(0.0f, value);
|
|
}
|
|
|
|
[Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")]
|
|
public float MeltdownDelay
|
|
{
|
|
get { return meltDownDelay; }
|
|
set { meltDownDelay = Math.Max(value, 0.0f); }
|
|
}
|
|
|
|
[Editable(0.0f, float.MaxValue), Serialize(30.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until the reactor catches fire.")]
|
|
public float FireDelay
|
|
{
|
|
get { return fireDelay; }
|
|
set { fireDelay = Math.Max(value, 0.0f); }
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")]
|
|
public float Temperature
|
|
{
|
|
get { return temperature; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
temperature = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Current fission rate of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
|
public float FissionRate
|
|
{
|
|
get { return fissionRate; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
fissionRate = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Current turbine output of the reactor (0% - 100%). Intended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
|
public float TurbineOutput
|
|
{
|
|
get { return turbineOutput; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
[Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)]
|
|
public float FuelConsumptionRate
|
|
{
|
|
get => fuelConsumptionRate;
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
fuelConsumptionRate = Math.Max(value, 0.0f);
|
|
}
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes, description: "Is the temperature currently critical. Intended to be used by StatusEffect conditionals (setting the value from XML has no effect).")]
|
|
public bool TemperatureCritical
|
|
{
|
|
get { return temperature > allowedTemperature.Y; }
|
|
set { /*do nothing*/ }
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes, description: "Is the automatic temperature control currently on. Indended to be used by StatusEffect conditionals (setting the value from XML is not recommended).")]
|
|
public bool AutoTemp
|
|
{
|
|
get { return autoTemp; }
|
|
set
|
|
{
|
|
autoTemp = value;
|
|
#if CLIENT
|
|
UpdateUIElementStates();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private float prevAvailableFuel;
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public float AvailableFuel { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public new float Load { get; private set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public float TargetFissionRate { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public float TargetTurbineOutput { get; set; }
|
|
|
|
[Serialize(0.0f, IsPropertySaveable.Yes)]
|
|
public float CorrectTurbineOutput { get; set; }
|
|
|
|
[Editable, Serialize(true, IsPropertySaveable.Yes)]
|
|
public bool ExplosionDamagesOtherSubs
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public bool MeltedDownThisRound { get; private set; }
|
|
|
|
public Reactor(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
IsActive = true;
|
|
InitProjSpecific(element);
|
|
}
|
|
|
|
partial void InitProjSpecific(ContentXElement element);
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null && nextServerLogWriteTime != null)
|
|
{
|
|
if (Timing.TotalTime >= (float)nextServerLogWriteTime)
|
|
{
|
|
GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " +
|
|
"Temperature: " + (int)(temperature * 100.0f) +
|
|
", Fission rate: " + (int)TargetFissionRate +
|
|
", Turbine output: " + (int)TargetTurbineOutput +
|
|
(autoTemp ? ", Autotemp ON" : ", Autotemp OFF"),
|
|
ServerLog.MessageType.ItemInteraction);
|
|
|
|
nextServerLogWriteTime = null;
|
|
lastServerLogWriteTime = (float)Timing.TotalTime;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
//if an AI character was using the item on the previous frame but not anymore, turn autotemp on
|
|
// (= bots turn autotemp back on when leaving the reactor)
|
|
if (LastAIUser != null)
|
|
{
|
|
if (LastAIUser.SelectedItem != item && LastAIUser.CanInteractWith(item))
|
|
{
|
|
AutoTemp = true;
|
|
if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
|
|
LastAIUser = null;
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
if (PowerOn && AvailableFuel < 1)
|
|
{
|
|
HintManager.OnReactorOutOfFuel(this);
|
|
}
|
|
#endif
|
|
|
|
float maxPowerOut = GetMaxOutput();
|
|
|
|
if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
|
|
{
|
|
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
|
|
#if CLIENT
|
|
FissionRateScrollBar.BarScroll = TargetFissionRate / 100.0f;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
signalControlledTargetFissionRate = null;
|
|
}
|
|
if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1)
|
|
{
|
|
TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f);
|
|
#if CLIENT
|
|
TurbineOutputScrollBar.BarScroll = TargetTurbineOutput / 100.0f;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
signalControlledTargetTurbineOutput = null;
|
|
}
|
|
|
|
static float adjustValueWithoutOverShooting(float current, float target, float speed)
|
|
{
|
|
return target < current ? Math.Max(target, current - speed) : Math.Min(target, current + speed);
|
|
}
|
|
|
|
prevAvailableFuel = AvailableFuel;
|
|
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
|
|
|
//use a smoothed "correct output" instead of the actual correct output based on the load
|
|
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
|
|
if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f))
|
|
{
|
|
CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
|
|
}
|
|
|
|
//calculate tolerances of the meters based on the skills of the user
|
|
//more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
|
|
float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
|
|
optimalTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
|
|
tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
|
|
allowedTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
|
|
|
|
optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
|
|
allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
|
|
|
|
optimalFissionRate = Vector2.Lerp(new Vector2(30, AvailableFuel - 20), new Vector2(20, AvailableFuel - 10), degreeOfSuccess);
|
|
optimalFissionRate.X = Math.Min(optimalFissionRate.X, optimalFissionRate.Y - 10);
|
|
allowedFissionRate = Vector2.Lerp(new Vector2(20, AvailableFuel), new Vector2(10, AvailableFuel), degreeOfSuccess);
|
|
allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
|
|
|
|
float heatAmount = GetGeneratedHeat(fissionRate);
|
|
|
|
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
|
|
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
|
|
temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime);
|
|
#if CLIENT
|
|
temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost;
|
|
#endif
|
|
|
|
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
|
|
|
|
TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime);
|
|
|
|
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
|
|
|
|
if (!PowerOn)
|
|
{
|
|
TargetFissionRate = 0.0f;
|
|
TargetTurbineOutput = 0.0f;
|
|
}
|
|
else if (autoTemp)
|
|
{
|
|
UpdateAutoTemp(2.0f, deltaTime);
|
|
}
|
|
|
|
|
|
float fuelLeft = 0.0f;
|
|
var containedItems = item.OwnInventory?.AllItems;
|
|
if (containedItems != null)
|
|
{
|
|
foreach (Item item in containedItems)
|
|
{
|
|
if (!item.HasTag(Tags.Fuel)) { continue; }
|
|
if (fissionRate > 0.0f)
|
|
{
|
|
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
|
|
Item.Submarine?.TeamID == CharacterTeamType.Team1 &&
|
|
Item.Submarine.GetConnectedSubs().Any(s => s.Info.IsOutpost && s.TeamID == CharacterTeamType.FriendlyNPC);
|
|
|
|
if (!isConnectedToFriendlyOutpost)
|
|
{
|
|
item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime;
|
|
}
|
|
}
|
|
fuelLeft += item.ConditionPercentage;
|
|
}
|
|
}
|
|
|
|
if (fissionRate > 0.0f)
|
|
{
|
|
if (item.AiTarget != null && maxPowerOut > 0)
|
|
{
|
|
var aiTarget = item.AiTarget;
|
|
float range = Math.Abs(currPowerConsumption) / maxPowerOut;
|
|
aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
|
|
if (item.CurrentHull != null)
|
|
{
|
|
var hullAITarget = item.CurrentHull.AiTarget;
|
|
if (hullAITarget != null)
|
|
{
|
|
hullAITarget.SoundRange = Math.Max(hullAITarget.SoundRange, aiTarget.SoundRange);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out");
|
|
item.SendSignal(((int)-CurrPowerConsumption).ToString(), "power_value_out");
|
|
item.SendSignal(((int)Load).ToString(), "load_value_out");
|
|
item.SendSignal(((int)AvailableFuel).ToString(), "fuel_out");
|
|
item.SendSignal(((int)fuelLeft).ToString(), "fuel_percentage_left");
|
|
|
|
UpdateFailures(deltaTime);
|
|
#if CLIENT
|
|
UpdateGraph(deltaTime);
|
|
#endif
|
|
AvailableFuel = 0.0f;
|
|
|
|
|
|
sendUpdateTimer -= deltaTime;
|
|
#if CLIENT
|
|
if (unsentChanges && sendUpdateTimer <= 0.0f)
|
|
#else
|
|
if (sendUpdateTimer < -NetworkUpdateIntervalLow || (unsentChanges && sendUpdateTimer <= 0.0f))
|
|
#endif
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null)
|
|
{
|
|
item.CreateServerEvent(this);
|
|
}
|
|
#elif CLIENT
|
|
if (GameMain.Client != null)
|
|
{
|
|
item.CreateClientEvent(this);
|
|
}
|
|
#endif
|
|
sendUpdateTimer = NetworkUpdateIntervalHigh;
|
|
unsentChanges = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a negative value (indicating the reactor generates power) when querying the power output connection.
|
|
/// </summary>
|
|
public override float GetCurrentPowerConsumption(Connection connection = null)
|
|
{
|
|
return connection != null && connection.IsPower && connection.IsOutput ? -1 : 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Min and Max power output of the reactor based on tolerance
|
|
/// </summary>
|
|
public override PowerRange MinMaxPowerOut(Connection conn, float load)
|
|
{
|
|
float tolerance = 1f;
|
|
|
|
//If within the optimal output allow for slight output adjustments
|
|
if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y &&
|
|
temperature > optimalTemperature.X && temperature < optimalTemperature.Y)
|
|
{
|
|
tolerance = 3f;
|
|
}
|
|
|
|
float maxPowerOut = GetMaxOutput();
|
|
|
|
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
|
|
float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
|
|
float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
|
|
|
|
minUpdatePowerOut = minOutput;
|
|
maxUpdatePowerOut = maxOutput;
|
|
|
|
float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut;
|
|
|
|
return new PowerRange(minOutput, maxOutput, reactorMax);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determine how much power to output based on the load. The load is divided between reactors according to their maximum output in multi-reactor setups.
|
|
/// </summary>
|
|
public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
|
|
{
|
|
//Load must be calculated at this stage instead of at gridResolved to remove influence of lower priority devices
|
|
float loadLeft = MathHelper.Max(load - power,0);
|
|
float expectedPower = MathHelper.Clamp(loadLeft, minMaxPower.Min, minMaxPower.Max);
|
|
|
|
//Delta ratio of Min and Max power output capability of the grid
|
|
float ratio = MathHelper.Max((loadLeft - minMaxPower.Min) / (minMaxPower.Max - minMaxPower.Min), 0);
|
|
if (float.IsInfinity(ratio))
|
|
{
|
|
ratio = 0;
|
|
}
|
|
|
|
float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut);
|
|
float newLoad = loadLeft;
|
|
|
|
float maxOutput = GetMaxOutput();
|
|
|
|
//Adjust behaviour for multi reactor setup
|
|
if (maxOutput != minMaxPower.ReactorMaxOutput)
|
|
{
|
|
float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft;
|
|
float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100);
|
|
newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft);
|
|
}
|
|
|
|
if (float.IsNegative(newLoad))
|
|
{
|
|
newLoad = 0.0f;
|
|
}
|
|
|
|
Load = newLoad;
|
|
currPowerConsumption = -output;
|
|
return output;
|
|
}
|
|
|
|
private float GetGeneratedHeat(float fissionRate)
|
|
{
|
|
return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f + temperatureBoost;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Do we need more fuel to generate enough power to match the current load.
|
|
/// </summary>
|
|
/// <param name="minimumOutputRatio">How low we allow the output/load ratio to go before loading more fuel.
|
|
/// 1.0 = always load more fuel when maximum output is too low, 0.5 = load more if max output is 50% of the load</param>
|
|
private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0)
|
|
{
|
|
float remainingFuel = item.ContainedItems.Sum(i => i.Condition);
|
|
if (remainingFuel <= minCondition && Load > 0.0f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
//fission rate is clamped to the amount of available fuel
|
|
float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f);
|
|
if (maxFissionRate >= 100.0f) { return false; }
|
|
|
|
float maxTurbineOutput = 100.0f;
|
|
|
|
//calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
|
|
float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
|
|
float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
|
|
float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput();
|
|
|
|
//maximum output not enough, we need more fuel
|
|
return theoreticalMaxOutput < Load * minimumOutputRatio;
|
|
}
|
|
|
|
private bool TooMuchFuel()
|
|
{
|
|
var containedItems = item.OwnInventory?.AllItems;
|
|
if (containedItems != null && containedItems.Count() <= 1) { return false; }
|
|
|
|
//get the amount of heat we'd generate if the fission rate was at the low end of the optimal range
|
|
float minimumHeat = GetGeneratedHeat(optimalFissionRate.X);
|
|
|
|
//if we need a very high turbine output to keep the engine from overheating, there's too much fuel
|
|
return minimumHeat > Math.Min(CorrectTurbineOutput * 1.5f, 90);
|
|
}
|
|
|
|
private void UpdateFailures(float deltaTime)
|
|
{
|
|
if (temperature > allowedTemperature.Y)
|
|
{
|
|
item.SendSignal("1", "meltdown_warning");
|
|
if (!item.InvulnerableToDamage)
|
|
{
|
|
//faster meltdown if the item is in a bad condition
|
|
meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
|
if (meltDownTimer > MeltdownDelay)
|
|
{
|
|
MeltDown();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
item.SendSignal("0", "meltdown_warning");
|
|
meltDownTimer = Math.Max(0.0f, meltDownTimer - deltaTime);
|
|
}
|
|
|
|
if (temperature > optimalTemperature.Y)
|
|
{
|
|
fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
|
|
#if SERVER
|
|
if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedItem == item)
|
|
{
|
|
GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
|
|
}
|
|
#endif
|
|
if (fireTimer >= FireDelay)
|
|
{
|
|
new FireSource(item.WorldPosition);
|
|
fireTimer = 0.0f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fireTimer = Math.Max(0.0f, fireTimer - deltaTime);
|
|
}
|
|
}
|
|
|
|
public void UpdateAutoTemp(float speed, float deltaTime)
|
|
{
|
|
float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f;
|
|
TargetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - TargetTurbineOutput, -speed, speed) * deltaTime;
|
|
TargetTurbineOutput = MathHelper.Clamp(TargetTurbineOutput, 0.0f, 100.0f);
|
|
|
|
float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
|
|
TargetFissionRate += MathHelper.Clamp(desiredFissionRate - TargetFissionRate, -speed, speed) * deltaTime;
|
|
|
|
if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f)
|
|
{
|
|
TargetFissionRate = Math.Min(TargetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
|
|
}
|
|
else if (-currPowerConsumption < Load)
|
|
{
|
|
TargetFissionRate = Math.Min(TargetFissionRate + speed * 2 * deltaTime, 100.0f);
|
|
}
|
|
TargetFissionRate = MathHelper.Clamp(TargetFissionRate, 0.0f, 100.0f);
|
|
|
|
//don't push the target too far from the current fission rate
|
|
//otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while
|
|
//for the actual fission rate and temperature to follow
|
|
TargetFissionRate = MathHelper.Clamp(TargetFissionRate, FissionRate - 5, FissionRate + 5);
|
|
}
|
|
|
|
public void PowerUpImmediately()
|
|
{
|
|
PowerOn = true;
|
|
AutoTemp = true;
|
|
prevAvailableFuel = AvailableFuel;
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
Update((float)(Timing.Step * 10.0f), cam: null);
|
|
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
|
|
AvailableFuel = prevAvailableFuel;
|
|
}
|
|
}
|
|
|
|
public override void UpdateBroken(float deltaTime, Camera cam)
|
|
{
|
|
base.UpdateBroken(deltaTime, cam);
|
|
|
|
item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out");
|
|
|
|
currPowerConsumption = 0.0f;
|
|
Temperature -= deltaTime * 1000.0f;
|
|
TargetFissionRate = Math.Max(TargetFissionRate - deltaTime * 10.0f, 0.0f);
|
|
TargetTurbineOutput = Math.Max(TargetTurbineOutput - deltaTime * 10.0f, 0.0f);
|
|
#if CLIENT
|
|
FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f;
|
|
TurbineOutputScrollBar.BarScroll = 1.0f - TurbineOutput / 100.0f;
|
|
UpdateGraph(deltaTime);
|
|
#endif
|
|
}
|
|
|
|
private void MeltDown()
|
|
{
|
|
if (item.Condition <= 0.0f) { return; }
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
|
|
if (!ExplosionDamagesOtherSubs && (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false))
|
|
{
|
|
foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
|
|
{
|
|
foreach (Explosion explosion in statusEffect.Explosions)
|
|
{
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
if (sub != item.Submarine) { explosion.IgnoredSubmarines.Add(sub); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
item.Condition = 0.0f;
|
|
fireTimer = 0.0f;
|
|
meltDownTimer = 0.0f;
|
|
MeltedDownThisRound = true;
|
|
|
|
var containedItems = item.OwnInventory?.AllItems;
|
|
if (containedItems != null)
|
|
{
|
|
foreach (Item containedItem in containedItems)
|
|
{
|
|
containedItem.Condition = 0.0f;
|
|
}
|
|
}
|
|
#if SERVER
|
|
GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
|
|
if (GameMain.Server != null)
|
|
{
|
|
GameMain.Server.KarmaManager.OnReactorMeltdown(item, blameOnBroken?.Character);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public override bool Pick(Character picker)
|
|
{
|
|
return picker != null;
|
|
}
|
|
|
|
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
|
|
character.AIController.SteeringManager.Reset();
|
|
bool shutDown = objective.Option == "shutdown";
|
|
|
|
IsActive = true;
|
|
|
|
if (!shutDown)
|
|
{
|
|
float degreeOfSuccess = Math.Min(DegreeOfSuccess(character), 1.0f);
|
|
float refuelLimit = 0.3f;
|
|
//characters with insufficient skill levels don't refuel the reactor
|
|
if (degreeOfSuccess > refuelLimit)
|
|
{
|
|
if (aiUpdateTimer > 0.0f)
|
|
{
|
|
aiUpdateTimer -= deltaTime;
|
|
return false;
|
|
}
|
|
aiUpdateTimer = AIUpdateInterval;
|
|
// load more fuel if the current maximum output is only 50% of the current load
|
|
// or if the fuel rod is (almost) deplenished
|
|
float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
|
|
if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
|
|
{
|
|
bool outOfFuel = false;
|
|
var container = item.GetComponent<ItemContainer>();
|
|
if (objective.SubObjectives.None())
|
|
{
|
|
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
|
|
containObjective.Completed += ReportFuelRodCount;
|
|
containObjective.Abandoned += ReportFuelRodCount;
|
|
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
|
|
|
|
void ReportFuelRodCount()
|
|
{
|
|
if (!character.IsOnPlayerTeam) { return; }
|
|
if (character.Submarine != Submarine.MainSub) { return; }
|
|
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
|
|
if (remainingFuelRods == 0)
|
|
{
|
|
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
|
|
outOfFuel = true;
|
|
}
|
|
else if (remainingFuelRods < 3)
|
|
{
|
|
character.Speak(TextManager.Get("DialogLowOnFuelRods").Value, null, 0.0f, "lowonfuelrods".ToIdentifier(), 30.0f);
|
|
}
|
|
}
|
|
}
|
|
return outOfFuel;
|
|
}
|
|
else
|
|
{
|
|
if (Item.ConditionPercentage <= 0 && AIObjectiveRepairItems.IsValidTarget(Item, character))
|
|
{
|
|
if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
|
|
{
|
|
objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
character.Speak(TextManager.Get("DialogReactorIsBroken").Value, identifier: "reactorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
|
}
|
|
}
|
|
if (TooMuchFuel())
|
|
{
|
|
DropFuel(minCondition: 0.1f, maxCondition: 100);
|
|
}
|
|
else
|
|
{
|
|
DropFuel(minCondition: 0, maxCondition: 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (objective.Override)
|
|
{
|
|
if (lastUser != null && lastUser != character && lastUser != LastAIUser)
|
|
{
|
|
if (lastUser.SelectedItem == item && character.IsOnPlayerTeam)
|
|
{
|
|
character.Speak(TextManager.Get("DialogReactorTaken").Value, null, 0.0f, "reactortaken".ToIdentifier(), 10.0f);
|
|
}
|
|
}
|
|
}
|
|
else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
LastUser = LastAIUser = character;
|
|
|
|
bool prevAutoTemp = autoTemp;
|
|
bool prevPowerOn = _powerOn;
|
|
float prevFissionRate = TargetFissionRate;
|
|
float prevTurbineOutput = TargetTurbineOutput;
|
|
|
|
if (shutDown)
|
|
{
|
|
PowerOn = false;
|
|
TargetFissionRate = 0.0f;
|
|
TargetTurbineOutput = 0.0f;
|
|
unsentChanges = true;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
PowerOn = true;
|
|
if (objective.Override || !autoTemp)
|
|
{
|
|
//characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
|
|
if (degreeOfSuccess < 0.5f)
|
|
{
|
|
AutoTemp = true;
|
|
}
|
|
else
|
|
{
|
|
AutoTemp = false;
|
|
UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
|
|
}
|
|
}
|
|
#if CLIENT
|
|
FissionRateScrollBar.BarScroll = FissionRate / 100.0f;
|
|
TurbineOutputScrollBar.BarScroll = TurbineOutput / 100.0f;
|
|
#endif
|
|
if (autoTemp != prevAutoTemp ||
|
|
prevPowerOn != _powerOn ||
|
|
Math.Abs(prevFissionRate - TargetFissionRate) > 1.0f ||
|
|
Math.Abs(prevTurbineOutput - TargetTurbineOutput) > 1.0f)
|
|
{
|
|
unsentChanges = true;
|
|
}
|
|
aiUpdateTimer = AIUpdateInterval;
|
|
return false;
|
|
}
|
|
|
|
|
|
void DropFuel(float minCondition, float maxCondition)
|
|
{
|
|
if (item.OwnInventory?.AllItems != null)
|
|
{
|
|
var container = item.GetComponent<ItemContainer>();
|
|
foreach (Item item in item.OwnInventory.AllItemsMod)
|
|
{
|
|
if (item.ConditionPercentage <= maxCondition && item.ConditionPercentage >= minCondition)
|
|
{
|
|
item.Drop(character);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
prevAvailableFuel = AvailableFuel;
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
switch (connection.Name)
|
|
{
|
|
case "shutdown":
|
|
if (TargetFissionRate > 0.0f || TargetTurbineOutput > 0.0f)
|
|
{
|
|
PowerOn = false;
|
|
AutoTemp = false;
|
|
TargetFissionRate = 0.0f;
|
|
TargetTurbineOutput = 0.0f;
|
|
registerUnsentChanges();
|
|
}
|
|
break;
|
|
case "set_fissionrate":
|
|
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
|
|
{
|
|
signalControlledTargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
|
|
lastReceivedFissionRateSignalTime = Timing.TotalTime;
|
|
registerUnsentChanges();
|
|
}
|
|
break;
|
|
case "set_turbineoutput":
|
|
if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
|
|
{
|
|
signalControlledTargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
|
|
lastReceivedTurbineOutputSignalTime = Timing.TotalTime;
|
|
registerUnsentChanges();
|
|
}
|
|
break;
|
|
}
|
|
|
|
void registerUnsentChanges()
|
|
{
|
|
if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; }
|
|
}
|
|
}
|
|
|
|
private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
|
|
private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
|
|
}
|
|
}
|