Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Xml.Linq;
12 using System.Globalization;
13 using MoonSharp.Interpreter;
14 using Barotrauma.Abilities;
15 
16 namespace Barotrauma
17 {
18  partial class CharacterHealth
19  {
20  public class LimbHealth
21  {
24 
26 
27  public readonly LocalizedString Name;
28 
29  //public readonly List<Affliction> Afflictions = new List<Affliction>();
30 
31  public readonly Dictionary<Identifier, float> VitalityMultipliers = new Dictionary<Identifier, float>();
32  public readonly Dictionary<Identifier, float> VitalityTypeMultipliers = new Dictionary<Identifier, float>();
33 
34  public LimbHealth() { }
35 
36  public LimbHealth(ContentXElement element, CharacterHealth characterHealth)
37  {
38  string limbName = element.GetAttributeString("name", null) ?? "generic";
39  if (limbName != "generic")
40  {
41  Name = TextManager.Get("HealthLimbName." + limbName);
42  }
43  foreach (var subElement in element.Elements())
44  {
45  switch (subElement.Name.ToString().ToLowerInvariant())
46  {
47  case "sprite":
48  IndicatorSprite = new Sprite(subElement);
49  HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
50  break;
51  case "highlightsprite":
52  HighlightSprite = new Sprite(subElement);
53  break;
54  case "vitalitymultiplier":
55  if (subElement.GetAttribute("name") != null)
56  {
57  DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.",
58  contentPackage: element.ContentPackage);
59  continue;
60  }
61  var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
62  if (vitalityMultipliers != null)
63  {
64  float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
65  foreach (var vitalityMultiplier in vitalityMultipliers)
66  {
67  VitalityMultipliers.Add(vitalityMultiplier, multiplier);
68  if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier))
69  {
70  DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?",
71  contentPackage: element.ContentPackage);
72  }
73  }
74  }
75  var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null);
76  if (vitalityTypeMultipliers != null)
77  {
78  float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
79  foreach (var vitalityTypeMultiplier in vitalityTypeMultipliers)
80  {
81  VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier);
82  if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier))
83  {
84  DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?",
85  contentPackage: element.ContentPackage);
86  }
87  }
88  }
89  if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
90  {
91  DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!",
92  contentPackage: element.ContentPackage);
93  }
94  break;
95  }
96  }
97  }
98  }
99 
100  public const float InsufficientOxygenThreshold = 30.0f;
101  public const float LowOxygenThreshold = 50.0f;
102  protected float minVitality;
103 
107  protected float UnmodifiedMaxVitality
108  {
110  set => Character.Params.Health.Vitality = value;
111  }
112 
113  public bool Unkillable;
114 
115  public bool DoesBleed
116  {
118  private set => Character.Params.Health.DoesBleed = value;
119  }
120 
121  public bool UseHealthWindow
122  {
124  set => Character.Params.Health.UseHealthWindow = value;
125  }
126 
127  public float CrushDepth
128  {
130  private set => Character.Params.Health.CrushDepth = value;
131  }
132 
133  private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
134 
135  private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
136  private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
137  private Affliction bloodlossAffliction;
138  private Affliction oxygenLowAffliction;
139  private Affliction pressureAffliction;
140  private Affliction stunAffliction;
141  public Affliction BloodlossAffliction { get => bloodlossAffliction; }
142 
143  public bool IsUnconscious
144  {
145  get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
146  }
147 
148  public float PressureKillDelay { get; private set; } = 5.0f;
149 
150  private float vitality;
151  public float Vitality
152  {
153  get
154  {
155  if (Character.IsDead)
156  {
157  return minVitality;
158  }
159  return vitality;
160  }
161  }
162 
167  public float VitalityDisregardingDeath => vitality;
168 
169  public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
170 
171  public float MaxVitality
172  {
173  get
174  {
175  float max = UnmodifiedMaxVitality;
176  if (Character?.Info?.Job?.Prefab != null)
177  {
179  }
181  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
182  {
184  ? campaign.Settings.CrewVitalityMultiplier
185  : campaign.Settings.NonCrewVitalityMultiplier;
186  }
187  max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
188  return max * Character.HealthMultiplier;
189  }
190  }
191 
192  public float MinVitality
193  {
194  get
195  {
196  if (Character?.Info?.Job?.Prefab != null)
197  {
198  return -MaxVitality;
199  }
200  return minVitality;
201  }
202  }
203 
204  public static readonly Color DefaultFaceTint = Color.TransparentBlack;
205 
206  public Color FaceTint
207  {
208  get;
209  private set;
210  }
211 
212  public Color BodyTint
213  {
214  get;
215  private set;
216  }
217 
218  public float OxygenAmount
219  {
220  get
221  {
222  if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; }
223  return -oxygenLowAffliction.Strength + 100;
224  }
225  set
226  {
227  if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; }
228  oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
229  }
230  }
231 
232  public float BloodlossAmount
233  {
234  get { return bloodlossAffliction.Strength; }
235  set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); }
236  }
237 
238  public float Stun
239  {
240  get { return stunAffliction.Strength; }
241  set
242  {
243  if (Character.GodMode) { return; }
244  stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength);
245  }
246  }
247 
248  public bool IsParalyzed { get; private set; }
249 
250  public float StunTimer { get; private set; }
251 
255  public bool WasInFullHealth { get; private set; }
256 
258  {
259  get { return pressureAffliction; }
260  }
261 
262  public readonly Character Character;
263 
264  public CharacterHealth(Character character)
265  {
266  this.Character = character;
267  vitality = 100.0f;
268 
269  DoesBleed = true;
270  UseHealthWindow = false;
271 
272  InitIrremovableAfflictions();
273 
274  limbHealths.Add(new LimbHealth());
275 
276  InitProjSpecific(null, character);
277  }
278 
279  public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null)
280  {
281  this.Character = character;
282  InitIrremovableAfflictions();
283 
284  vitality = UnmodifiedMaxVitality;
285 
286  minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f);
287 
288  limbHealths.Clear();
289  limbHealthElement ??= element;
290  foreach (var subElement in limbHealthElement.Elements())
291  {
292  if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
293  limbHealths.Add(new LimbHealth(subElement, this));
294  }
295  if (limbHealths.Count == 0)
296  {
297  limbHealths.Add(new LimbHealth());
298  }
299 
300  InitProjSpecific(element, character);
301  }
302 
303  private void InitIrremovableAfflictions()
304  {
305  irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
306  irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
307  irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
308  irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
309  foreach (Affliction affliction in irremovableAfflictions)
310  {
311  afflictions.Add(affliction, null);
312  }
313  }
314 
315  partial void InitProjSpecific(ContentXElement element, Character character);
316 
317  public IReadOnlyCollection<Affliction> GetAllAfflictions()
318  {
319  return afflictions.Keys;
320  }
321 
322  public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
323  {
324  return afflictions.Keys.Where(limbHealthFilter);
325  }
326 
327  private float GetTotalDamage(LimbHealth limbHealth)
328  {
329  float totalDamage = 0.0f;
330  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
331  {
332  if (kvp.Value != limbHealth) { continue; }
333  var affliction = kvp.Key;
334  totalDamage += affliction.GetVitalityDecrease(this);
335  }
336  return totalDamage;
337  }
338 
339  private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
340  private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
341 
342  public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) =>
343  GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions);
344 
345  public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true)
346  => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
347 
348  public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true)
349  => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
350 
351  private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
352  {
353  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
354  {
355  if (!allowLimbAfflictions && kvp.Value != null) { continue; }
356  if (predicate(kvp.Key)) { return kvp.Key; }
357  }
358  return null;
359  }
360 
361  public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
362  {
363  return GetAffliction(identifier, allowLimbAfflictions) as T;
364  }
365 
366  public Affliction GetAffliction(Identifier identifier, Limb limb)
367  {
368  if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
369  {
370  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
371  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
372  return null;
373  }
374  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
375  {
376  if (limbHealths[limb.HealthIndex] == kvp.Value && kvp.Key.Prefab.Identifier == identifier) { return kvp.Key; }
377  }
378  return null;
379  }
380 
381  public Limb GetAfflictionLimb(Affliction affliction)
382  {
383  if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
384  {
385  if (limbHealth == null) { return null; }
386  int limbHealthIndex = limbHealths.IndexOf(limbHealth);
387  foreach (Limb limb in Character.AnimController.Limbs)
388  {
389  if (limb.HealthIndex == limbHealthIndex) { return limb; }
390  }
391  }
392  return null;
393  }
394 
402  public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
403  {
404  if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
405 
406  float strength = 0.0f;
407  LimbHealth limbHealth = limbHealths[limb.HealthIndex];
408  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
409  {
410  if (kvp.Value == limbHealth)
411  {
412  Affliction affliction = kvp.Key;
413  if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
414  if (affliction.Prefab.AfflictionType == afflictionType)
415  {
416  strength += affliction.Strength;
417  }
418  }
419  }
420  return strength;
421  }
422 
423  public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true)
424  {
425  return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions);
426  }
427 
428  public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true)
429  {
430  return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions);
431  }
432 
433  public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true)
434  {
435  float strength = 0.0f;
436  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
437  {
438  if (!allowLimbAfflictions && kvp.Value != null) { continue; }
439  var affliction = kvp.Key;
440  if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
441  if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) &&
442  (affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty))
443  {
444  strength += affliction.Strength;
445  }
446  }
447  return strength;
448  }
449 
450  public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
451  {
452  if (Character.GodMode) { return; }
453  if (!ignoreUnkillability)
454  {
455  if (!affliction.Prefab.IsBuff && Unkillable) { return; }
456  }
457  if (affliction.Prefab.LimbSpecific)
458  {
459  if (targetLimb == null)
460  {
461  //if a limb-specific affliction is applied to no specific limb, apply to all limbs
462  foreach (LimbHealth limbHealth in limbHealths)
463  {
464  AddLimbAffliction(limbHealth, affliction, allowStacking: allowStacking);
465  }
466 
467  }
468  else
469  {
470  AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking);
471  }
472  }
473  else
474  {
475  AddAffliction(affliction, allowStacking: allowStacking);
476  }
477  }
478 
479  public float GetResistance(AfflictionPrefab afflictionPrefab)
480  {
481  // This is a % resistance (0 to 1.0)
482  float resistance = 0.0f;
483  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
484  {
485  var affliction = kvp.Key;
486  resistance += affliction.GetResistance(afflictionPrefab.Identifier);
487  }
488  // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
489  float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
490  // The returned value is calculated to be a % resistance again
491  return 1 - ((1 - resistance) * abilityResistanceMultiplier);
492  }
493 
494  public float GetStatValue(StatTypes statType)
495  {
496  float value = 0f;
497  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
498  {
499  var affliction = kvp.Key;
500  value += affliction.GetStatValue(statType);
501  }
502  return value;
503  }
504 
505  public bool HasFlag(AbilityFlags flagType)
506  {
507  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
508  {
509  var affliction = kvp.Key;
510  if (affliction.HasFlag(flagType)) { return true; }
511  }
512  return false;
513  }
514 
515  private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
516 
517  public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
518  {
519  matchingAfflictions.Clear();
520  matchingAfflictions.AddRange(afflictions.Keys);
521 
522  ReduceMatchingAfflictions(amount, treatmentAction);
523  }
524 
525  public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
526  {
527  if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
528 
529  matchingAfflictions.Clear();
530  foreach (var affliction in afflictions)
531  {
532  if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
533  {
534  matchingAfflictions.Add(affliction.Key);
535  }
536  }
537 
538  ReduceMatchingAfflictions(amount, treatmentAction, attacker);
539  }
540 
541  private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
542  => afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]);
543 
544  public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null)
545  {
546  if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
547 
548  matchingAfflictions.Clear();
549  matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
550 
551  ReduceMatchingAfflictions(amount, treatmentAction);
552  }
553 
554  public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
555  {
556  if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
557  if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
558 
559  matchingAfflictions.Clear();
560  var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
561  foreach (var affliction in afflictions)
562  {
563  if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
564  affliction.Value == targetLimbHealth)
565  {
566  matchingAfflictions.Add(affliction.Key);
567  }
568  }
569  ReduceMatchingAfflictions(amount, treatmentAction, attacker);
570  }
571 
572  private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
573  {
574  if (matchingAfflictions.Count == 0) { return; }
575 
576  float reduceAmount = amount / matchingAfflictions.Count;
577 
578  if (reduceAmount > 0f)
579  {
580  var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount);
581  attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction);
582  reduceAmount = abilityReduceAffliction.Value;
583  }
584 
585  for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
586  {
587  var matchingAffliction = matchingAfflictions[i];
588 
589  if (matchingAffliction.Strength < reduceAmount)
590  {
591  float surplus = reduceAmount - matchingAffliction.Strength;
592  amount -= matchingAffliction.Strength;
593  matchingAffliction.Strength = 0.0f;
594  matchingAfflictions.RemoveAt(i);
595  if (i == 0) { i = matchingAfflictions.Count; }
596  if (i > 0) { reduceAmount += surplus / i; }
597  AchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
598  }
599  else
600  {
601  matchingAffliction.Strength -= reduceAmount;
602  amount -= reduceAmount;
603  if (treatmentAction != null)
604  {
605  if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
606  {
607  matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
608  }
609  else if (treatmentAction.Value == ActionType.OnFailure)
610  {
611  matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
612  }
613  }
614  }
615  }
617  }
618 
619  public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true)
620  {
621  if (Unkillable || Character.GodMode) { return; }
622  if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
623  {
624  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
625  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
626  return;
627  }
628 
629  var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyDamage", this, attackResult, hitLimb, allowStacking);
630 
631  if (should != null && should.Value)
632  return;
633 
634  foreach (Affliction newAffliction in attackResult.Afflictions)
635  {
636  if (newAffliction.Prefab.LimbSpecific)
637  {
638  AddLimbAffliction(hitLimb, newAffliction, allowStacking);
639  }
640  else
641  {
642  AddAffliction(newAffliction, allowStacking);
643  }
644  }
645  }
646 
647  private void KillIfOutOfVitality()
648  {
649  if (Vitality <= MinVitality &&
650  !Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
651  {
652  Kill();
653  }
654  }
655 
656  private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
657  private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
658  public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
659  {
660  if (Unkillable || Character.GodMode) { return; }
661 
662  afflictionsToRemove.Clear();
663  afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
664  a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
665  a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
666  a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
667  foreach (var affliction in afflictionsToRemove)
668  {
669  afflictions.Remove(affliction);
670  }
671 
672  foreach (LimbHealth limbHealth in limbHealths)
673  {
674  if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
675  if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
676  if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
677  }
678 
680  KillIfOutOfVitality();
681  }
682 
683  public float GetLimbDamage(Limb limb, Identifier afflictionType)
684  {
685  float damageStrength;
686  if (limb.IsSevered)
687  {
688  return 1;
689  }
690  else
691  {
692  // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
693  // Therefore with e.g. 80 health, the max damage per limb would be 40.
694  // Having at least 40 damage on both legs would cause maximum limping.
695  float max = MaxVitality / 2;
696  if (afflictionType.IsEmpty)
697  {
698  float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
699  float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
700  float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
701  damageStrength = Math.Min(damage + bleeding + burn, max);
702  }
703  else
704  {
705  damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
706  }
707  return damageStrength / max;
708  }
709  }
710 
711  public void RemoveAllAfflictions()
712  {
713  afflictionsToRemove.Clear();
714  afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
715  foreach (var affliction in afflictionsToRemove)
716  {
717  afflictions.Remove(affliction);
718  }
719  foreach (Affliction affliction in irremovableAfflictions)
720  {
721  affliction.Strength = 0.0f;
722  }
724  }
725 
727  {
728  afflictionsToRemove.Clear();
729  afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
730  !irremovableAfflictions.Contains(a) &&
731  !a.Prefab.IsBuff &&
732  a.Prefab.AfflictionType != "geneticmaterialbuff" &&
733  a.Prefab.AfflictionType != "geneticmaterialdebuff"));
734  foreach (var affliction in afflictionsToRemove)
735  {
736  afflictions.Remove(affliction);
737  }
738  foreach (Affliction affliction in irremovableAfflictions)
739  {
740  affliction.Strength = 0.0f;
741  }
743  }
744 
745  private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true)
746  {
747  if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
748  if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
749  {
750  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
751  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
752  return;
753  }
754  AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction, allowStacking);
755  }
756 
757  private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction, bool allowStacking = true)
758  {
759  if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
760  if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
761  if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
762  if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
763  {
764  if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
765  {
766  return;
767  }
768  }
770  {
771  if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
772  {
773  return;
774  }
775  }
776  if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
777  if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
778  if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
779 
780  var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyAffliction", this, limbHealth, newAffliction, allowStacking);
781 
782  if (should != null && should.Value)
783  return;
784 
785  Affliction existingAffliction = null;
786  foreach ((Affliction affliction, LimbHealth value) in afflictions)
787  {
788  if (value == limbHealth && affliction.Prefab == newAffliction.Prefab)
789  {
790  existingAffliction = affliction;
791  break;
792  }
793  }
794 
795  if (existingAffliction != null)
796  {
797  float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab));
798  if (allowStacking)
799  {
800  // Add the existing strength
801  newStrength += existingAffliction.Strength;
802  }
803  newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
804  if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
805  existingAffliction.Strength = newStrength;
806  existingAffliction.Duration = existingAffliction.Prefab.Duration;
807  if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
809  KillIfOutOfVitality();
810  return;
811  }
812 
813  //create a new instance of the affliction to make sure we don't use the same instance for multiple characters
814  //or modify the affliction instance of an Attack or a StatusEffect
815  var copyAffliction = newAffliction.Prefab.Instantiate(
816  Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab))),
817  newAffliction.Source);
818  afflictions.Add(copyAffliction, limbHealth);
819  AchievementManager.OnAfflictionReceived(copyAffliction, Character);
820  MedicalClinic.OnAfflictionCountChanged(Character);
821 
823 
825  KillIfOutOfVitality();
826 #if CLIENT
827  if (OpenHealthWindow != this && limbHealth != null)
828  {
829  selectedLimbIndex = -1;
830  }
831 #endif
832  }
833 
834  private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
835  {
836  AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
837  }
838 
839  partial void UpdateSkinTint();
840 
841  partial void UpdateLimbAfflictionOverlays();
842 
843  public void Update(float deltaTime)
844  {
845  WasInFullHealth = vitality >= MaxVitality;
846 
847  UpdateOxygen(deltaTime);
848 
849  StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
850 
851  if (!Character.GodMode)
852  {
853  afflictionsToRemove.Clear();
854  afflictionsToUpdate.Clear();
855  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
856  {
857  var affliction = kvp.Key;
858  if (affliction.Strength <= 0.0f)
859  {
860  AchievementManager.OnAfflictionRemoved(affliction, Character);
861  if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
862  continue;
863  }
864  if (affliction.Prefab.Duration > 0.0f)
865  {
866  affliction.Duration -= deltaTime;
867  if (affliction.Duration <= 0.0f)
868  {
869  afflictionsToRemove.Add(affliction);
870  continue;
871  }
872  }
873  afflictionsToUpdate.Add(kvp);
874  }
875  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
876  {
877  var affliction = kvp.Key;
878  Limb targetLimb = null;
879  if (kvp.Value != null)
880  {
881  int healthIndex = limbHealths.IndexOf(kvp.Value);
882  targetLimb =
883  Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
885  }
886  affliction.Update(this, targetLimb, deltaTime);
887  affliction.DamagePerSecondTimer += deltaTime;
888  if (affliction is AfflictionBleeding bleeding)
889  {
890  UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
891  }
892  Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
893  }
894 
895  foreach (var affliction in afflictionsToRemove)
896  {
897  afflictions.Remove(affliction);
898  }
899 
900  if (afflictionsToRemove.Count is not 0)
901  {
902  MedicalClinic.OnAfflictionCountChanged(Character);
903  }
904  }
905 
907  if (Character.InWater)
908  {
910  }
911  else
912  {
914  }
915 
916  UpdateDamageReductions(deltaTime);
917 
918  if (!Character.GodMode)
919  {
920 #if CLIENT
921  if (Character.IsVisible)
922  {
923  UpdateLimbAfflictionOverlays();
924  UpdateSkinTint();
925  }
926 #endif
928  KillIfOutOfVitality();
929  }
930  }
931 
932  public void ForceUpdateVisuals()
933  {
934  UpdateLimbAfflictionOverlays();
935  UpdateSkinTint();
936  }
937 
938  private void UpdateDamageReductions(float deltaTime)
939  {
941  if (healthRegen > 0)
942  {
943  ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), healthRegen * deltaTime);
944  }
945  float burnReduction = Character.Params.Health.BurnReduction;
946  if (burnReduction > 0)
947  {
948  ReduceAfflictionOnAllLimbs("burn".ToIdentifier(), burnReduction * deltaTime);
949  }
950  float bleedingReduction = Character.Params.Health.BleedingReduction;
951  if (bleedingReduction > 0)
952  {
953  ReduceAfflictionOnAllLimbs("bleeding".ToIdentifier(), bleedingReduction * deltaTime);
954  }
955  }
956 
960  public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab);
961 
962  private void UpdateOxygen(float deltaTime)
963  {
964  if (!Character.NeedsOxygen)
965  {
966  oxygenLowAffliction.Strength = 0.0f;
967  return;
968  }
969 
970  float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
971  float prevOxygen = OxygenAmount;
972  if (IsUnconscious)
973  {
974  //clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
975  float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
976  //the character dies of oxygen deprivation in 100 seconds after losing consciousness
977  OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
978  }
979  else
980  {
981  float decreaseSpeed = -5.0f;
982  float increaseSpeed = 10.0f;
983  decreaseSpeed *= (1f - oxygenlowResistance);
984  increaseSpeed *= (1f + oxygenlowResistance);
985  float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
986  if (holdBreathMultiplier <= -1.0f)
987  {
988  OxygenAmount = -100.0f;
989  }
990  else
991  {
992  decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
993  OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
994  }
995  }
996 
997  UpdateOxygenProjSpecific(prevOxygen, deltaTime);
998  }
999 
1000  partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
1001 
1002  partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
1003 
1004  public void SetVitality(float newVitality)
1005  {
1006  UnmodifiedMaxVitality = newVitality;
1008  }
1009 
1010  public void CalculateVitality()
1011  {
1012  vitality = MaxVitality;
1013  IsParalyzed = false;
1014  if (Unkillable || Character.GodMode) { return; }
1015 
1016  foreach (var (affliction, limbHealth) in afflictions)
1017  {
1018  float vitalityDecrease = affliction.GetVitalityDecrease(this);
1019  if (limbHealth != null)
1020  {
1021  vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
1022  }
1023  vitality -= vitalityDecrease;
1024  affliction.CalculateDamagePerSecond(vitalityDecrease);
1025 
1026  if (affliction.Strength >= affliction.Prefab.MaxStrength &&
1027  affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
1028  {
1029  IsParalyzed = true;
1030  }
1031  }
1032 #if CLIENT
1033  if (IsUnconscious)
1034  {
1035  HintManager.OnCharacterUnconscious(Character);
1036  }
1037 #endif
1038  }
1039 
1040  private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
1041  {
1042  float multiplier = 1.0f;
1043  if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
1044  {
1045  multiplier *= vitalityMultiplier;
1046  }
1047  if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier))
1048  {
1049  multiplier *= vitalityTypeMultiplier;
1050  }
1051  return multiplier;
1052  }
1053 
1057  private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction)
1058  {
1059  float vitalityDecrease = affliction.GetVitalityDecrease(this);
1060  if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null)
1061  {
1062  vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
1063  }
1064  return vitalityDecrease;
1065  }
1066 
1067  private void Kill()
1068  {
1069  if (Unkillable || Character.GodMode) { return; }
1070 
1071  var (type, affliction) = GetCauseOfDeath();
1072  UpdateLimbAfflictionOverlays();
1073  UpdateSkinTint();
1074  Character.Kill(type, affliction);
1075 
1076  WasInFullHealth = false;
1077 #if CLIENT
1078  DisplayVitalityDelay = 0.0f;
1080 #endif
1081  }
1082 
1083  // We need to use another list of the afflictions when we call the status effects triggered by afflictions,
1084  // because those status effects may add or remove other afflictions while iterating the collection.
1085  private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
1087  {
1088  afflictionsCopy.Clear();
1089  afflictionsCopy.AddRange(afflictions.Keys);
1090  foreach (Affliction affliction in afflictionsCopy)
1091  {
1092  affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
1093  }
1094  }
1095 
1096  public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath()
1097  {
1098  List<Affliction> currentAfflictions = GetAllAfflictions(true);
1099 
1100  Affliction strongestAffliction = null;
1101  float largestStrength = 0.0f;
1102  foreach (Affliction affliction in currentAfflictions)
1103  {
1104  if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
1105  {
1106  strongestAffliction = affliction;
1107  largestStrength = affliction.GetVitalityDecrease(this);
1108  }
1109  }
1110 
1111  CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
1112  if (strongestAffliction == oxygenLowAffliction)
1113  {
1114  causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
1115  }
1116 
1117  return (causeOfDeath, strongestAffliction);
1118  }
1119 
1120  private readonly List<Affliction> allAfflictions = new List<Affliction>();
1121  private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
1122  {
1123  allAfflictions.Clear();
1124  if (!mergeSameAfflictions)
1125  {
1126  allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
1127  }
1128  else
1129  {
1130  foreach (Affliction affliction in afflictions.Keys)
1131  {
1132  if (predicate != null && !predicate(affliction)) { continue; }
1133  var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
1134  if (existingAffliction == null)
1135  {
1136  var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
1137  if (affliction.Source != null) { newAffliction.Source = affliction.Source; }
1138  newAffliction.DamagePerSecond = affliction.DamagePerSecond;
1139  newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
1140  allAfflictions.Add(newAffliction);
1141  }
1142  else
1143  {
1144  existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
1145  existingAffliction.Strength += affliction.Strength;
1146  }
1147  }
1148  }
1149  return allAfflictions;
1150  }
1151 
1158  public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
1159  {
1160  //key = item identifier
1161  //float = suitability
1162  treatmentSuitability.Clear();
1163  float minSuitability = -10, maxSuitability = 10;
1164  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1165  {
1166  var affliction = kvp.Key;
1167  var limbHealth = kvp.Value;
1168  if (limb != null &&
1169  affliction.Prefab.LimbSpecific &&
1170  GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb))
1171  {
1172  if (limbHealth == null) { continue; }
1173  int healthIndex = limbHealths.IndexOf(limbHealth);
1174  if (limb.HealthIndex != healthIndex) { continue; }
1175  }
1176 
1177  float strength = affliction.Strength;
1178  if (predictFutureDuration > 0.0f)
1179  {
1180  strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
1181  }
1182 
1183  //other afflictions of the same type increase the "treatability"
1184  // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
1185  float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
1186 
1187  if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
1188 
1189  if (ignoreHiddenAfflictions)
1190  {
1191  if (user == Character)
1192  {
1193  if (strength < affliction.Prefab.ShowIconThreshold) { continue; }
1194  }
1195  else
1196  {
1197  if (strength < affliction.Prefab.ShowIconToOthersThreshold) { continue; }
1198  }
1199  }
1200 
1201  foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
1202  {
1203  float suitability = treatment.Value * strength;
1204  if (suitability > 0)
1205  {
1206  //if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
1207  //if the suitability is negative though, we need to take it into account!
1208  //otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
1209  if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
1210  }
1211  if (treatment.Value > strength)
1212  {
1213  //avoid using very effective meds on small injuries
1214  float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
1215  suitability /= overtreatmentFactor;
1216  }
1217  if (!treatmentSuitability.ContainsKey(treatment.Key))
1218  {
1219  treatmentSuitability[treatment.Key] = suitability;
1220  }
1221  else
1222  {
1223  treatmentSuitability[treatment.Key] += suitability;
1224  }
1225  minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
1226  maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
1227  }
1228  }
1229  }
1230 
1237  public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
1238  {
1239  float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
1240  if (affliction.Prefab.LimbSpecific)
1241  {
1242  foreach (Affliction otherAffliction in afflictions.Keys)
1243  {
1244  if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
1245  {
1246  totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
1247  }
1248  }
1249  }
1250  return totalAfflictionStrength;
1251  }
1252 
1253  private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
1254  public IEnumerable<Identifier> GetActiveAfflictionTags()
1255  {
1256  afflictionTags.Clear();
1257  foreach (Affliction affliction in afflictions.Keys)
1258  {
1259  var currentEffect = affliction.GetActiveEffect();
1260  if (currentEffect is { Tag.IsEmpty: false })
1261  {
1262  afflictionTags.Add(currentEffect.Tag);
1263  }
1264  }
1265  return afflictionTags;
1266  }
1267 
1268  public float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb = null)
1269  {
1270  float strength = affliction.Strength;
1271  foreach (var statusEffect in StatusEffect.DurationList)
1272  {
1273  if (!statusEffect.Targets.Any(t => t == Character || (limb != null && Character.AnimController.Limbs.Contains(t)))) { continue; }
1274  float statusEffectDuration = Math.Min(statusEffect.Timer, predictFutureDuration);
1275  foreach (var statusEffectAffliction in statusEffect.Parent.Afflictions)
1276  {
1277  if (statusEffectAffliction.Prefab == affliction.Prefab)
1278  {
1279  strength += statusEffectAffliction.Strength * statusEffectDuration;
1280  }
1281  }
1282  foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction)
1283  {
1284  if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier ||
1285  statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType)
1286  {
1287  strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration;
1288  }
1289  }
1290  }
1291  return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
1292  }
1293 
1294  private readonly List<Affliction> activeAfflictions = new List<Affliction>();
1295  private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
1296  public void ServerWrite(IWriteMessage msg)
1297  {
1298  activeAfflictions.Clear();
1299  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1300  {
1301  var affliction = kvp.Key;
1302  var limbHealth = kvp.Value;
1303  if (limbHealth != null) { continue; }
1304  if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
1305  {
1306  activeAfflictions.Add(affliction);
1307  }
1308  }
1309  msg.WriteByte((byte)activeAfflictions.Count);
1310  foreach (Affliction affliction in activeAfflictions)
1311  {
1312  msg.WriteUInt32(affliction.Prefab.UintIdentifier);
1313  msg.WriteRangedSingle(
1314  MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
1315  0.0f, affliction.Prefab.MaxStrength, 8);
1316  msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
1317  foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
1318  {
1319  msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], 0, periodicEffect.MaxInterval, 8);
1320  }
1321  }
1322 
1323  limbAfflictions.Clear();
1324  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1325  {
1326  var limbAffliction = kvp.Key;
1327  var limbHealth = kvp.Value;
1328  if (limbHealth == null) { continue; }
1329  if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) { continue; }
1330  limbAfflictions.Add((limbHealth, limbAffliction));
1331  }
1332 
1333  msg.WriteByte((byte)limbAfflictions.Count);
1334  foreach (var (limbHealth, affliction) in limbAfflictions)
1335  {
1336  msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1);
1337  msg.WriteUInt32(affliction.Prefab.UintIdentifier);
1338  msg.WriteRangedSingle(
1339  MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
1340  0.0f, affliction.Prefab.MaxStrength, 8);
1341  msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
1342  foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
1343  {
1344  msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
1345  }
1346  }
1347  }
1348 
1349  public void Remove()
1350  {
1351  RemoveProjSpecific();
1352  afflictionsToRemove.Clear();
1353  afflictionsToUpdate.Clear();
1354  }
1355 
1356  partial void RemoveProjSpecific();
1357 
1361  public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
1362  afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength);
1363 
1364  public void Save(XElement healthElement)
1365  {
1366  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1367  {
1368  var affliction = kvp.Key;
1369  var limbHealth = kvp.Value;
1370  if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
1371  if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
1372  healthElement.Add(new XElement("Affliction",
1373  new XAttribute("identifier", affliction.Identifier),
1374  new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
1375  }
1376 
1377  for (int i = 0; i < limbHealths.Count; i++)
1378  {
1379  var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
1380  healthElement.Add(limbHealthElement);
1381  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions.Where(a => a.Value == limbHealths[i]))
1382  {
1383  var affliction = kvp.Key;
1384  var limbHealth = kvp.Value;
1385  if (affliction.Strength <= 0.0f) { continue; }
1386  limbHealthElement.Add(new XElement("Affliction",
1387  new XAttribute("identifier", affliction.Identifier),
1388  new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
1389  }
1390  }
1391  }
1392 
1393  public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
1394  {
1395  foreach (var subElement in element.Elements())
1396  {
1397  switch (subElement.Name.ToString().ToLowerInvariant())
1398  {
1399  case "affliction":
1400  LoadAffliction(subElement);
1401  break;
1402  case "limbhealth":
1403  int limbHealthIndex = subElement.GetAttributeInt("i", -1);
1404  if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
1405  {
1406  DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
1407  continue;
1408  }
1409  foreach (XElement afflictionElement in subElement.Elements())
1410  {
1411  LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
1412  }
1413  break;
1414  }
1415  }
1416 
1417  void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
1418  {
1419  string id = afflictionElement.GetAttributeString("identifier", "");
1420  var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
1421  if (afflictionPrefab == null)
1422  {
1423  DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
1424  return;
1425  }
1426  if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
1427  float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
1428  var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
1429  if (irremovableAffliction != null)
1430  {
1431  irremovableAffliction.Strength = strength;
1432  }
1433  else
1434  {
1435  afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
1436  }
1437  }
1438  }
1439  }
1440 }
A special affliction type that increases the character's Bloodloss affliction with a rate relative to...
readonly Dictionary< AfflictionPrefab.PeriodicEffect, float > PeriodicEffectTimers
Definition: Affliction.cs:79
Identifier Identifier
Definition: Affliction.cs:62
float GetVitalityDecrease(CharacterHealth characterHealth)
Definition: Affliction.cs:171
float GetResistance(Identifier afflictionId)
Definition: Affliction.cs:340
float GetStatValue(StatTypes statType)
Definition: Affliction.cs:369
virtual float Strength
Definition: Affliction.cs:31
AfflictionPrefab.Effect GetActiveEffect()
Definition: Affliction.cs:160
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
Definition: Affliction.cs:461
PeriodicEffect applies StatusEffects to the character periodically.
AfflictionPrefab is a prefab that defines a type of affliction that can be applied to a character....
Affliction Instantiate(float strength, Character source=null)
IList< PeriodicEffect > PeriodicEffects
static readonly Identifier BleedingType
static AfflictionPrefab OxygenLow
readonly bool IsBuff
If set to true, the game will recognize this affliction as a buff. This means, among other things,...
static AfflictionPrefab InternalDamage
static readonly Identifier DamageType
static readonly Identifier BurnType
readonly LimbType IndicatorLimb
If the affliction doesn't affect individual limbs, this attribute determines where the game will rend...
static AfflictionPrefab Burn
readonly bool LimbSpecific
If set to true, the affliction affects individual limbs. Otherwise, it affects the whole character.
readonly float MaxStrength
The maximum strength this affliction can have.
static AfflictionPrefab Bleeding
static readonly Identifier ParalysisType
readonly Identifier AfflictionType
Arbitrary string that is used to identify the type of the affliction.
static readonly PrefabCollection< AfflictionPrefab > Prefabs
static AfflictionPrefab Stun
readonly float ActivationThreshold
How high the strength has to be for the affliction to take effect
static AfflictionPrefab Pressure
static AfflictionPrefab Bloodloss
LimbHealth(ContentXElement element, CharacterHealth characterHealth)
void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking=true, bool ignoreUnkillability=false)
float VitalityDisregardingDeath
How much vitality the character would have if it was alive? E.g. a character killed by disconnection ...
void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions=true)
Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions=true)
float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
Get the total strength of the afflictions of a specific type attached to a specific limb
void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
static IEnumerable< Affliction > SortAfflictionsBySeverity(IEnumerable< Affliction > afflictions, bool excludeBuffs=true)
Automatically filters out buffs.
void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction=null)
IEnumerable< Affliction > GetAllAfflictions(Func< Affliction, bool > limbHealthFilter)
void Load(XElement element, Func< AfflictionPrefab, bool > afflictionPredicate=null)
void GetSuitableTreatments(Dictionary< Identifier, float > treatmentSuitability, Character user, Limb limb=null, bool ignoreHiddenAfflictions=false, float predictFutureDuration=0.0f)
Get the identifiers of the items that can be used to treat the character. Takes into account all the ...
float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier=0.3f, bool includeSameAffliction=true)
Returns the total strength of instances of the same affliction on all the characters limbs,...
void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking=true)
float UnmodifiedMaxVitality
Maximum vitality without talent- or job-based modifiers
float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions=true)
CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement=null)
Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions=true)
float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb=null)
bool WasInFullHealth
Was the character in full health at the beginning of the frame?
void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction=null)
float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions=true)
T GetAffliction< T >(Identifier identifier, bool allowLimbAfflictions=true)
Affliction GetAffliction(string identifier, bool allowLimbAfflictions=true)
bool GodMode
Godmoded characters cannot receive any afflictions whatsoever
void SetStun(float newStun, bool allowStunDecrease=false, bool isNetworkMessage=false)
float GetStatValue(StatTypes statType, bool includeSaved=true)
float HealthMultiplier
Can be used to modify the character's health via StatusEffects
void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage=false, bool log=true)
float HumanPrefabHealthMultiplier
Health multiplier of the human prefab this character is an instance of (if any)
bool IsVisible
Is the character currently visible on the camera. Refresh the value by calling DoVisibilityCheck.
IEnumerable< Identifier > ImmunityIdentifiers
void Add(ContentXElement elem)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
static GameSession?? GameSession
Definition: GameMain.cs:88
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
JobPrefab Prefab
Definition: Job.cs:18
object Call(string name, params object[] args)
readonly Identifier Identifier
Definition: Prefab.cs:34
Limb GetLimb(LimbType limbType, bool excludeSevered=true)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
static readonly List< DurationListElement > DurationList
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
Definition: Enums.cs:615
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:19
AbilityEffectType
Definition: Enums.cs:125
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180