Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Globalization;
6 using System.Linq;
7 
9 {
11  {
12  const float NetworkUpdateIntervalHigh = 0.5f;
13 
14  const float TemperatureBoostAmount = 25;
15 
16  //the rate at which the reactor is being run on (higher rate -> higher temperature)
17  private float fissionRate;
18 
19  //how much of the generated steam is used to spin the turbines and generate power
20  private float turbineOutput;
21 
22  private float temperature;
23 
24  //is automatic temperature control on
25  //(adjusts the fission rate and turbine output automatically to keep the
26  //amount of power generated balanced with the load)
27  private bool autoTemp;
28 
29  private float fuelConsumptionRate;
30 
31  private float meltDownTimer, meltDownDelay;
32  private float fireTimer, fireDelay;
33 
34  private float maxPowerOutput;
35  private float minUpdatePowerOut;
36  private float maxUpdatePowerOut;
37 
38  private bool unsentChanges;
39  private float sendUpdateTimer;
40 
41  private float degreeOfSuccess;
42 
43  private Vector2 optimalTemperature, allowedTemperature;
44  private Vector2 optimalFissionRate, allowedFissionRate;
45  private Vector2 optimalTurbineOutput, allowedTurbineOutput;
46 
47  private float? signalControlledTargetFissionRate, signalControlledTargetTurbineOutput;
48  private double lastReceivedFissionRateSignalTime, lastReceivedTurbineOutputSignalTime;
49 
50  private float temperatureBoost;
51 
52  public bool AllowTemperatureBoost => Math.Abs(temperatureBoost) < TemperatureBoostAmount * 0.9f;
53 
54  private bool _powerOn;
55 
56  [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
57  public bool PowerOn
58  {
59  get { return _powerOn; }
60  set
61  {
62  _powerOn = value;
63 #if CLIENT
64  UpdateUIElementStates();
65 #endif
66  }
67  }
68 
69  protected override PowerPriority Priority { get { return PowerPriority.Reactor; } }
70 
71  public Character LastAIUser { get; private set; }
72 
73  [Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes)]
74  public bool LastUserWasPlayer { get; private set; }
75 
76  private Character lastUser;
78  {
79  get { return lastUser; }
80  private set
81  {
82  if (lastUser == value) { return; }
83  lastUser = value;
84  if (lastUser == null)
85  {
86  degreeOfSuccess = 0.0f;
87  LastUserWasPlayer = false;
88  }
89  else
90  {
91  degreeOfSuccess = Math.Min(DegreeOfSuccess(lastUser), 1.0f);
92  LastUserWasPlayer = lastUser.IsPlayer;
93  }
94  }
95  }
96 
97  [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)]
98  public float MaxPowerOutput
99  {
100  get => maxPowerOutput;
101  set => maxPowerOutput = Math.Max(0.0f, value);
102  }
103 
104  [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")]
105  public float MeltdownDelay
106  {
107  get { return meltDownDelay; }
108  set { meltDownDelay = Math.Max(value, 0.0f); }
109  }
110 
111  [Editable(0.0f, float.MaxValue), Serialize(30.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until the reactor catches fire.")]
112  public float FireDelay
113  {
114  get { return fireDelay; }
115  set { fireDelay = Math.Max(value, 0.0f); }
116  }
117 
118  [Serialize(0.0f, IsPropertySaveable.Yes, description: "Current temperature of the reactor (0% - 100%). Indended to be used by StatusEffect conditionals.")]
119  public float Temperature
120  {
121  get { return temperature; }
122  set
123  {
124  if (!MathUtils.IsValid(value)) return;
125  temperature = MathHelper.Clamp(value, 0.0f, 100.0f);
126  }
127  }
128 
129  [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).")]
130  public float FissionRate
131  {
132  get { return fissionRate; }
133  set
134  {
135  if (!MathUtils.IsValid(value)) return;
136  fissionRate = MathHelper.Clamp(value, 0.0f, 100.0f);
137  }
138  }
139 
140  [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).")]
141  public float TurbineOutput
142  {
143  get { return turbineOutput; }
144  set
145  {
146  if (!MathUtils.IsValid(value)) return;
147  turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f);
148  }
149  }
150 
151  [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)]
152  public float FuelConsumptionRate
153  {
154  get => fuelConsumptionRate;
155  set
156  {
157  if (!MathUtils.IsValid(value)) return;
158  fuelConsumptionRate = Math.Max(value, 0.0f);
159  }
160  }
161 
162  [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).")]
164  {
165  get { return temperature > allowedTemperature.Y; }
166  set { /*do nothing*/ }
167  }
168 
169  [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).")]
170  public bool AutoTemp
171  {
172  get { return autoTemp; }
173  set
174  {
175  autoTemp = value;
176 #if CLIENT
177  UpdateUIElementStates();
178 #endif
179  }
180  }
181 
182  private float prevAvailableFuel;
183 
184  [Serialize(0.0f, IsPropertySaveable.Yes)]
185  public float AvailableFuel { get; set; }
186 
187  [Serialize(0.0f, IsPropertySaveable.Yes)]
188  public new float Load { get; private set; }
189 
190  [Serialize(0.0f, IsPropertySaveable.Yes)]
191  public float TargetFissionRate { get; set; }
192 
193  [Serialize(0.0f, IsPropertySaveable.Yes)]
194  public float TargetTurbineOutput { get; set; }
195 
196  [Serialize(0.0f, IsPropertySaveable.Yes)]
197  public float CorrectTurbineOutput { get; set; }
198 
201  {
202  get;
203  set;
204  }
205 
206  public bool MeltedDownThisRound { get; private set; }
207 
208  public Reactor(Item item, ContentXElement element)
209  : base(item, element)
210  {
211  IsActive = true;
212  InitProjSpecific(element);
213  }
214 
215  partial void InitProjSpecific(ContentXElement element);
216 
217  public override void Update(float deltaTime, Camera cam)
218  {
219 #if SERVER
220  if (GameMain.Server != null && nextServerLogWriteTime != null)
221  {
222  if (Timing.TotalTime >= (float)nextServerLogWriteTime)
223  {
224  GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " +
225  "Temperature: " + (int)(temperature * 100.0f) +
226  ", Fission rate: " + (int)TargetFissionRate +
227  ", Turbine output: " + (int)TargetTurbineOutput +
228  (autoTemp ? ", Autotemp ON" : ", Autotemp OFF"),
229  ServerLog.MessageType.ItemInteraction);
230 
231  nextServerLogWriteTime = null;
232  lastServerLogWriteTime = (float)Timing.TotalTime;
233  }
234  }
235 #endif
236 
237  //if an AI character was using the item on the previous frame but not anymore, turn autotemp on
238  // (= bots turn autotemp back on when leaving the reactor)
239  if (LastAIUser != null)
240  {
242  {
243  AutoTemp = true;
244  if (GameMain.NetworkMember?.IsServer ?? false) { unsentChanges = true; }
245  LastAIUser = null;
246  }
247  }
248 
249 #if CLIENT
250  if (PowerOn && AvailableFuel < 1)
251  {
252  HintManager.OnReactorOutOfFuel(this);
253  }
254 #endif
255 
256  float maxPowerOut = GetMaxOutput();
257 
258  if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
259  {
260  TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
261 #if CLIENT
263 #endif
264  }
265  else
266  {
267  signalControlledTargetFissionRate = null;
268  }
269  if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1)
270  {
271  TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f);
272 #if CLIENT
274 #endif
275  }
276  else
277  {
278  signalControlledTargetTurbineOutput = null;
279  }
280 
281  static float adjustValueWithoutOverShooting(float current, float target, float speed)
282  {
283  return target < current ? Math.Max(target, current - speed) : Math.Min(target, current + speed);
284  }
285 
286  prevAvailableFuel = AvailableFuel;
287  ApplyStatusEffects(ActionType.OnActive, deltaTime);
288 
289  //use a smoothed "correct output" instead of the actual correct output based on the load
290  //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
291  if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f))
292  {
293  CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime;
294  }
295 
296  //calculate tolerances of the meters based on the skills of the user
297  //more skilled characters have larger "sweet spots", making it easier to keep the power output at a suitable level
298  float tolerance = MathHelper.Lerp(2.5f, 10.0f, degreeOfSuccess);
299  optimalTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
300  tolerance = MathHelper.Lerp(5.0f, 20.0f, degreeOfSuccess);
301  allowedTurbineOutput = new Vector2(CorrectTurbineOutput - tolerance, CorrectTurbineOutput + tolerance);
302 
303  optimalTemperature = Vector2.Lerp(new Vector2(40.0f, 60.0f), new Vector2(30.0f, 70.0f), degreeOfSuccess);
304  allowedTemperature = Vector2.Lerp(new Vector2(30.0f, 70.0f), new Vector2(10.0f, 90.0f), degreeOfSuccess);
305 
306  optimalFissionRate = Vector2.Lerp(new Vector2(30, AvailableFuel - 20), new Vector2(20, AvailableFuel - 10), degreeOfSuccess);
307  optimalFissionRate.X = Math.Min(optimalFissionRate.X, optimalFissionRate.Y - 10);
308  allowedFissionRate = Vector2.Lerp(new Vector2(20, AvailableFuel), new Vector2(10, AvailableFuel), degreeOfSuccess);
309  allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
310 
311  float heatAmount = GetGeneratedHeat(fissionRate);
312 
313  float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
314  Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
315  temperatureBoost = adjustValueWithoutOverShooting(temperatureBoost, 0.0f, deltaTime);
316 #if CLIENT
317  temperatureBoostUpButton.Enabled = temperatureBoostDownButton.Enabled = AllowTemperatureBoost;
318 #endif
319 
320  FissionRate = MathHelper.Lerp(fissionRate, Math.Min(TargetFissionRate, AvailableFuel), deltaTime);
321 
322  TurbineOutput = MathHelper.Lerp(turbineOutput, TargetTurbineOutput, deltaTime);
323 
324  float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
325 
326  if (!PowerOn)
327  {
328  TargetFissionRate = 0.0f;
329  TargetTurbineOutput = 0.0f;
330  }
331  else if (autoTemp)
332  {
333  UpdateAutoTemp(2.0f, deltaTime);
334  }
335 
336 
337  float fuelLeft = 0.0f;
338  var containedItems = item.OwnInventory?.AllItems;
339  if (containedItems != null)
340  {
341  foreach (Item item in containedItems)
342  {
343  if (!item.HasTag(Tags.Fuel)) { continue; }
344  if (fissionRate > 0.0f)
345  {
346  bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
347  Item.Submarine?.TeamID == CharacterTeamType.Team1 &&
348  Item.Submarine.GetConnectedSubs().Any(s => s.Info.IsOutpost && s.TeamID == CharacterTeamType.FriendlyNPC);
349 
350  if (!isConnectedToFriendlyOutpost)
351  {
352  item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime;
353  }
354  }
355  fuelLeft += item.ConditionPercentage;
356  }
357  }
358 
359  if (fissionRate > 0.0f)
360  {
361  if (item.AiTarget != null && maxPowerOut > 0)
362  {
363  var aiTarget = item.AiTarget;
364  float range = Math.Abs(currPowerConsumption) / maxPowerOut;
365  aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range);
366  if (item.CurrentHull != null)
367  {
368  var hullAITarget = item.CurrentHull.AiTarget;
369  if (hullAITarget != null)
370  {
371  hullAITarget.SoundRange = Math.Max(hullAITarget.SoundRange, aiTarget.SoundRange);
372  }
373  }
374  }
375  }
376 
377  item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out");
378  item.SendSignal(((int)-CurrPowerConsumption).ToString(), "power_value_out");
379  item.SendSignal(((int)Load).ToString(), "load_value_out");
380  item.SendSignal(((int)AvailableFuel).ToString(), "fuel_out");
381  item.SendSignal(((int)fuelLeft).ToString(), "fuel_percentage_left");
382 
383  UpdateFailures(deltaTime);
384 #if CLIENT
385  UpdateGraph(deltaTime);
386 #endif
387  AvailableFuel = 0.0f;
388 
389 
390  sendUpdateTimer -= deltaTime;
391 #if CLIENT
392  if (unsentChanges && sendUpdateTimer <= 0.0f)
393 #else
394  if (sendUpdateTimer < -NetworkUpdateIntervalLow || (unsentChanges && sendUpdateTimer <= 0.0f))
395 #endif
396  {
397 #if SERVER
398  if (GameMain.Server != null)
399  {
400  item.CreateServerEvent(this);
401  }
402 #elif CLIENT
403  if (GameMain.Client != null)
404  {
405  item.CreateClientEvent(this);
406  }
407 #endif
408  sendUpdateTimer = NetworkUpdateIntervalHigh;
409  unsentChanges = false;
410  }
411  }
412 
416  public override float GetCurrentPowerConsumption(Connection connection = null)
417  {
418  return connection != null && connection.IsPower && connection.IsOutput ? -1 : 0;
419  }
420 
424  public override PowerRange MinMaxPowerOut(Connection conn, float load)
425  {
426  float tolerance = 1f;
427 
428  //If within the optimal output allow for slight output adjustments
429  if (turbineOutput > optimalTurbineOutput.X && turbineOutput < optimalTurbineOutput.Y &&
430  temperature > optimalTemperature.X && temperature < optimalTemperature.Y)
431  {
432  tolerance = 3f;
433  }
434 
435  float maxPowerOut = GetMaxOutput();
436 
437  float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
438  float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1);
439  float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor);
440 
441  minUpdatePowerOut = minOutput;
442  maxUpdatePowerOut = maxOutput;
443 
444  float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut;
445 
446  return new PowerRange(minOutput, maxOutput, reactorMax);
447  }
448 
452  public override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
453  {
454  //Load must be calculated at this stage instead of at gridResolved to remove influence of lower priority devices
455  float loadLeft = MathHelper.Max(load - power,0);
456  float expectedPower = MathHelper.Clamp(loadLeft, minMaxPower.Min, minMaxPower.Max);
457 
458  //Delta ratio of Min and Max power output capability of the grid
459  float ratio = MathHelper.Max((loadLeft - minMaxPower.Min) / (minMaxPower.Max - minMaxPower.Min), 0);
460  if (float.IsInfinity(ratio))
461  {
462  ratio = 0;
463  }
464 
465  float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut);
466  float newLoad = loadLeft;
467 
468  float maxOutput = GetMaxOutput();
469 
470  //Adjust behaviour for multi reactor setup
471  if (maxOutput != minMaxPower.ReactorMaxOutput)
472  {
473  float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft;
474  float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100);
475  newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft);
476  }
477 
478  if (float.IsNegative(newLoad))
479  {
480  newLoad = 0.0f;
481  }
482 
483  Load = newLoad;
484  currPowerConsumption = -output;
485  return output;
486  }
487 
488  private float GetGeneratedHeat(float fissionRate)
489  {
490  return fissionRate * (prevAvailableFuel / 100.0f) * 2.0f + temperatureBoost;
491  }
492 
498  private bool NeedMoreFuel(float minimumOutputRatio, float minCondition = 0)
499  {
500  float remainingFuel = item.ContainedItems.Sum(i => i.Condition);
501  if (remainingFuel <= minCondition && Load > 0.0f)
502  {
503  return true;
504  }
505 
506  //fission rate is clamped to the amount of available fuel
507  float maxFissionRate = Math.Min(prevAvailableFuel, 100.0f);
508  if (maxFissionRate >= 100.0f) { return false; }
509 
510  float maxTurbineOutput = 100.0f;
511 
512  //calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max
513  float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate);
514  float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f);
515  float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput();
516 
517  //maximum output not enough, we need more fuel
518  return theoreticalMaxOutput < Load * minimumOutputRatio;
519  }
520 
521  private bool TooMuchFuel()
522  {
523  var containedItems = item.OwnInventory?.AllItems;
524  if (containedItems != null && containedItems.Count() <= 1) { return false; }
525 
526  //get the amount of heat we'd generate if the fission rate was at the low end of the optimal range
527  float minimumHeat = GetGeneratedHeat(optimalFissionRate.X);
528 
529  //if we need a very high turbine output to keep the engine from overheating, there's too much fuel
530  return minimumHeat > Math.Min(CorrectTurbineOutput * 1.5f, 90);
531  }
532 
533  private void UpdateFailures(float deltaTime)
534  {
535  if (temperature > allowedTemperature.Y)
536  {
537  item.SendSignal("1", "meltdown_warning");
539  {
540  //faster meltdown if the item is in a bad condition
541  meltDownTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
542  if (meltDownTimer > MeltdownDelay)
543  {
544  MeltDown();
545  return;
546  }
547  }
548  }
549  else
550  {
551  item.SendSignal("0", "meltdown_warning");
552  meltDownTimer = Math.Max(0.0f, meltDownTimer - deltaTime);
553  }
554 
555  if (temperature > optimalTemperature.Y)
556  {
557  fireTimer += MathHelper.Lerp(deltaTime * 2.0f, deltaTime, item.Condition / item.MaxCondition);
558 #if SERVER
559  if (fireTimer > Math.Min(5.0f, FireDelay / 2) && blameOnBroken?.Character?.SelectedItem == item)
560  {
561  GameMain.Server.KarmaManager.OnReactorOverHeating(item, blameOnBroken.Character, deltaTime);
562  }
563 #endif
564  if (fireTimer >= FireDelay)
565  {
566  new FireSource(item.WorldPosition);
567  fireTimer = 0.0f;
568  }
569  }
570  else
571  {
572  fireTimer = Math.Max(0.0f, fireTimer - deltaTime);
573  }
574  }
575 
576  public void UpdateAutoTemp(float speed, float deltaTime)
577  {
578  float desiredTurbineOutput = (optimalTurbineOutput.X + optimalTurbineOutput.Y) / 2.0f;
579  TargetTurbineOutput += MathHelper.Clamp(desiredTurbineOutput - TargetTurbineOutput, -speed, speed) * deltaTime;
580  TargetTurbineOutput = MathHelper.Clamp(TargetTurbineOutput, 0.0f, 100.0f);
581 
582  float desiredFissionRate = (optimalFissionRate.X + optimalFissionRate.Y) / 2.0f;
583  TargetFissionRate += MathHelper.Clamp(desiredFissionRate - TargetFissionRate, -speed, speed) * deltaTime;
584 
585  if (temperature > (optimalTemperature.X + optimalTemperature.Y) / 2.0f)
586  {
587  TargetFissionRate = Math.Min(TargetFissionRate - speed * 2 * deltaTime, allowedFissionRate.Y);
588  }
589  else if (-currPowerConsumption < Load)
590  {
591  TargetFissionRate = Math.Min(TargetFissionRate + speed * 2 * deltaTime, 100.0f);
592  }
593  TargetFissionRate = MathHelper.Clamp(TargetFissionRate, 0.0f, 100.0f);
594 
595  //don't push the target too far from the current fission rate
596  //otherwise we may "overshoot", cranking the target fission rate all the way up because it takes a while
597  //for the actual fission rate and temperature to follow
598  TargetFissionRate = MathHelper.Clamp(TargetFissionRate, FissionRate - 5, FissionRate + 5);
599  }
600 
601  public void PowerUpImmediately()
602  {
603  PowerOn = true;
604  AutoTemp = true;
605  prevAvailableFuel = AvailableFuel;
606  for (int i = 0; i < 100; i++)
607  {
608  Update((float)(Timing.Step * 10.0f), cam: null);
609  UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
610  AvailableFuel = prevAvailableFuel;
611  }
612  }
613 
614  public override void UpdateBroken(float deltaTime, Camera cam)
615  {
616  base.UpdateBroken(deltaTime, cam);
617 
618  item.SendSignal(((int)(temperature * 100.0f)).ToString(), "temperature_out");
619 
620  currPowerConsumption = 0.0f;
621  Temperature -= deltaTime * 1000.0f;
622  TargetFissionRate = Math.Max(TargetFissionRate - deltaTime * 10.0f, 0.0f);
623  TargetTurbineOutput = Math.Max(TargetTurbineOutput - deltaTime * 10.0f, 0.0f);
624 #if CLIENT
625  FissionRateScrollBar.BarScroll = 1.0f - FissionRate / 100.0f;
627  UpdateGraph(deltaTime);
628 #endif
629  }
630 
631  private void MeltDown()
632  {
633  if (item.Condition <= 0.0f) { return; }
634  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
635 
636  if (!ExplosionDamagesOtherSubs && (statusEffectLists?.ContainsKey(ActionType.OnBroken) ?? false))
637  {
638  foreach (var statusEffect in statusEffectLists[ActionType.OnBroken])
639  {
640  foreach (Explosion explosion in statusEffect.Explosions)
641  {
642  foreach (Submarine sub in Submarine.Loaded)
643  {
644  if (sub != item.Submarine) { explosion.IgnoredSubmarines.Add(sub); }
645  }
646  }
647  }
648  }
649 
650  item.Condition = 0.0f;
651  fireTimer = 0.0f;
652  meltDownTimer = 0.0f;
653  MeltedDownThisRound = true;
654 
655  var containedItems = item.OwnInventory?.AllItems;
656  if (containedItems != null)
657  {
658  foreach (Item containedItem in containedItems)
659  {
660  containedItem.Condition = 0.0f;
661  }
662  }
663 #if SERVER
664  GameServer.Log("Reactor meltdown!", ServerLog.MessageType.ItemInteraction);
665  if (GameMain.Server != null)
666  {
667  GameMain.Server.KarmaManager.OnReactorMeltdown(item, blameOnBroken?.Character);
668  }
669 #endif
670  }
671 
672  public override bool Pick(Character picker)
673  {
674  return picker != null;
675  }
676 
677  public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
678  {
679  if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
680  character.AIController.SteeringManager.Reset();
681  bool shutDown = objective.Option == "shutdown";
682 
683  IsActive = true;
684 
685  if (!shutDown)
686  {
687  float degreeOfSuccess = Math.Min(DegreeOfSuccess(character), 1.0f);
688  float refuelLimit = 0.3f;
689  //characters with insufficient skill levels don't refuel the reactor
690  if (degreeOfSuccess > refuelLimit)
691  {
692  if (aiUpdateTimer > 0.0f)
693  {
694  aiUpdateTimer -= deltaTime;
695  return false;
696  }
698  // load more fuel if the current maximum output is only 50% of the current load
699  // or if the fuel rod is (almost) deplenished
700  float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2);
701  if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition))
702  {
703  bool outOfFuel = false;
704  var container = item.GetComponent<ItemContainer>();
705  if (objective.SubObjectives.None())
706  {
707  var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
708  containObjective.Completed += ReportFuelRodCount;
709  containObjective.Abandoned += ReportFuelRodCount;
710  character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
711 
712  void ReportFuelRodCount()
713  {
714  if (!character.IsOnPlayerTeam) { return; }
715  if (character.Submarine != Submarine.MainSub) { return; }
716  int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
717  if (remainingFuelRods == 0)
718  {
719  character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
720  outOfFuel = true;
721  }
722  else if (remainingFuelRods < 3)
723  {
724  character.Speak(TextManager.Get("DialogLowOnFuelRods").Value, null, 0.0f, "lowonfuelrods".ToIdentifier(), 30.0f);
725  }
726  }
727  }
728  return outOfFuel;
729  }
730  else
731  {
733  {
734  if (Item.Repairables.Average(r => r.DegreeOfSuccess(character)) > 0.4f)
735  {
736  objective.AddSubObjective(new AIObjectiveRepairItem(character, Item, objective.objectiveManager, isPriority: true));
737  return false;
738  }
739  else
740  {
741  character.Speak(TextManager.Get("DialogReactorIsBroken").Value, identifier: "reactorisbroken".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
742  }
743  }
744  if (TooMuchFuel())
745  {
746  DropFuel(minCondition: 0.1f, maxCondition: 100);
747  }
748  else
749  {
750  DropFuel(minCondition: 0, maxCondition: 0);
751  }
752  }
753  }
754  }
755 
756  if (objective.Override)
757  {
758  if (lastUser != null && lastUser != character && lastUser != LastAIUser)
759  {
760  if (lastUser.SelectedItem == item && character.IsOnPlayerTeam)
761  {
762  character.Speak(TextManager.Get("DialogReactorTaken").Value, null, 0.0f, "reactortaken".ToIdentifier(), 10.0f);
763  }
764  }
765  }
766  else if (LastUserWasPlayer && lastUser != null && lastUser.TeamID == character.TeamID)
767  {
768  return true;
769  }
770 
771  LastUser = LastAIUser = character;
772 
773  bool prevAutoTemp = autoTemp;
774  bool prevPowerOn = _powerOn;
775  float prevFissionRate = TargetFissionRate;
776  float prevTurbineOutput = TargetTurbineOutput;
777 
778  if (shutDown)
779  {
780  PowerOn = false;
781  TargetFissionRate = 0.0f;
782  TargetTurbineOutput = 0.0f;
783  unsentChanges = true;
784  return true;
785  }
786  else
787  {
788  PowerOn = true;
789  if (objective.Override || !autoTemp)
790  {
791  //characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually
792  if (degreeOfSuccess < 0.5f)
793  {
794  AutoTemp = true;
795  }
796  else
797  {
798  AutoTemp = false;
799  UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f);
800  }
801  }
802 #if CLIENT
805 #endif
806  if (autoTemp != prevAutoTemp ||
807  prevPowerOn != _powerOn ||
808  Math.Abs(prevFissionRate - TargetFissionRate) > 1.0f ||
809  Math.Abs(prevTurbineOutput - TargetTurbineOutput) > 1.0f)
810  {
811  unsentChanges = true;
812  }
814  return false;
815  }
816 
817 
818  void DropFuel(float minCondition, float maxCondition)
819  {
820  if (item.OwnInventory?.AllItems != null)
821  {
822  var container = item.GetComponent<ItemContainer>();
823  foreach (Item item in item.OwnInventory.AllItemsMod)
824  {
825  if (item.ConditionPercentage <= maxCondition && item.ConditionPercentage >= minCondition)
826  {
827  item.Drop(character);
828  break;
829  }
830  }
831  }
832  }
833  }
834 
835  public override void OnMapLoaded()
836  {
837  prevAvailableFuel = AvailableFuel;
838  }
839 
840  public override void ReceiveSignal(Signal signal, Connection connection)
841  {
842  switch (connection.Name)
843  {
844  case "shutdown":
845  if (TargetFissionRate > 0.0f || TargetTurbineOutput > 0.0f)
846  {
847  PowerOn = false;
848  AutoTemp = false;
849  TargetFissionRate = 0.0f;
850  TargetTurbineOutput = 0.0f;
851  registerUnsentChanges();
852  }
853  break;
854  case "set_fissionrate":
855  if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate))
856  {
857  signalControlledTargetFissionRate = MathHelper.Clamp(newFissionRate, 0.0f, 100.0f);
858  lastReceivedFissionRateSignalTime = Timing.TotalTime;
859  registerUnsentChanges();
860  }
861  break;
862  case "set_turbineoutput":
863  if (PowerOn && float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput))
864  {
865  signalControlledTargetTurbineOutput = MathHelper.Clamp(newTurbineOutput, 0.0f, 100.0f);
866  lastReceivedTurbineOutputSignalTime = Timing.TotalTime;
867  registerUnsentChanges();
868  }
869  break;
870  }
871 
872  void registerUnsentChanges()
873  {
874  if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; }
875  }
876  }
877 
878  private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
879  private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
880  }
881 }
void AddSubObjective(AIObjective objective, bool addFirst=false)
override bool IsValidTarget(Item item)
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
bool CanInteractWith(Character c, float maxDist=200.0f, bool checkVisibility=true, bool skipDistanceCheck=false)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
AITarget AiTarget
Definition: Entity.cs:55
Submarine Submarine
Definition: Entity.cs:53
Explosions are area of effect attacks that can damage characters, items and structures.
readonly HashSet< Submarine > IgnoredSubmarines
override bool Enabled
Definition: GUIButton.cs:27
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
IEnumerable< Item > AllItemsMod
All items contained in the inventory. Allows modifying the contents of the inventory while being enum...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
void SendSignal(string signal, string connectionName)
void ApplyStatusEffects(ActionType type, float deltaTime, Character character=null, Limb targetLimb=null, Entity useTarget=null, Character user=null, Vector2? worldPosition=null, float afflictionMultiplier=1.0f)
readonly Dictionary< ActionType, List< StatusEffect > > statusEffectLists
float DegreeOfSuccess(Character character)
Returns 0.0f-1.0f based on how well the Character can use the itemcomponent
float currPowerConsumption
The amount of power currently consumed by the item. Negative values mean that the item is providing p...
override PowerRange MinMaxPowerOut(Connection conn, float load)
Min and Max power output of the reactor based on tolerance
override bool Pick(Character picker)
a Character has picked the item
override float GetConnectionPowerOut(Connection conn, float power, PowerRange minMaxPower, float load)
Determine how much power to output based on the load. The load is divided between reactors according ...
override float GetCurrentPowerConsumption(Connection connection=null)
Returns a negative value (indicating the reactor generates power) when querying the power output conn...
override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
true if the operation was completed
override void OnMapLoaded()
Called when all items have been loaded. Use to initialize connections between items.
static bool IsLoadedOutpost
Is there a loaded level set and is it an outpost?
List< Item > GetItems(bool alsoFromConnectedSubs)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
Interface for entities that the clients can send events to the server
Interface for entities that the server can send events to the clients
PowerPriority
Order in which power sources will provide to a grid, lower number is higher priority
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
readonly float ReactorMaxOutput
Used by reactors to communicate their maximum output to each other so they can divide the grid load b...