Server LuaCsForBarotrauma
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 
25  public Rectangle HighlightArea;
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 
261  public bool ShowDamageOverlay = true;
262 
264  {
265  get { return pressureAffliction; }
266  }
267 
268  public readonly Character Character;
269 
270  public CharacterHealth(Character character)
271  {
272  this.Character = character;
273  vitality = 100.0f;
274 
275  DoesBleed = true;
276  UseHealthWindow = false;
277 
278  InitIrremovableAfflictions();
279 
280  limbHealths.Add(new LimbHealth());
281 
282  InitProjSpecific(null, character);
283  }
284 
285  public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null)
286  {
287  this.Character = character;
288  InitIrremovableAfflictions();
289 
290  vitality = UnmodifiedMaxVitality;
291 
292  minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f);
293 
294  limbHealths.Clear();
295  limbHealthElement ??= element;
296  foreach (var subElement in limbHealthElement.Elements())
297  {
298  if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
299  limbHealths.Add(new LimbHealth(subElement, this));
300  }
301  if (limbHealths.Count == 0)
302  {
303  limbHealths.Add(new LimbHealth());
304  }
305 
306  InitProjSpecific(element, character);
307  }
308 
309  private void InitIrremovableAfflictions()
310  {
311  irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
312  irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
313  irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
314  irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
315  foreach (Affliction affliction in irremovableAfflictions)
316  {
317  afflictions.Add(affliction, null);
318  }
319  }
320 
321  partial void InitProjSpecific(ContentXElement element, Character character);
322 
323  public IReadOnlyCollection<Affliction> GetAllAfflictions()
324  {
325  return afflictions.Keys;
326  }
327 
328  public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
329  {
330  return afflictions.Keys.Where(limbHealthFilter);
331  }
332 
333  private float GetTotalDamage(LimbHealth limbHealth)
334  {
335  float totalDamage = 0.0f;
336  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
337  {
338  if (kvp.Value != limbHealth) { continue; }
339  var affliction = kvp.Key;
340  totalDamage += affliction.GetVitalityDecrease(this);
341  }
342  return totalDamage;
343  }
344 
345  private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
346  private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
347 
348  public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) =>
349  GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions);
350 
351  public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true)
352  => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
353 
354  public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true)
355  => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
356 
357  private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
358  {
359  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
360  {
361  if (!allowLimbAfflictions && kvp.Value != null) { continue; }
362  if (predicate(kvp.Key)) { return kvp.Key; }
363  }
364  return null;
365  }
366 
367  public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
368  {
369  return GetAffliction(identifier, allowLimbAfflictions) as T;
370  }
371 
372  public Affliction GetAffliction(Identifier identifier, Limb limb)
373  {
374  if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
375  {
376  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
377  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
378  return null;
379  }
380  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
381  {
382  if (limbHealths[limb.HealthIndex] == kvp.Value && kvp.Key.Prefab.Identifier == identifier) { return kvp.Key; }
383  }
384  return null;
385  }
386 
387  public Limb GetAfflictionLimb(Affliction affliction)
388  {
389  if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
390  {
391  if (limbHealth == null) { return null; }
392  int limbHealthIndex = limbHealths.IndexOf(limbHealth);
393  foreach (Limb limb in Character.AnimController.Limbs)
394  {
395  if (limb.HealthIndex == limbHealthIndex) { return limb; }
396  }
397  }
398  return null;
399  }
400 
408  public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
409  {
410  if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
411 
412  float strength = 0.0f;
413  LimbHealth limbHealth = limbHealths[limb.HealthIndex];
414  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
415  {
416  if (kvp.Value == limbHealth)
417  {
418  Affliction affliction = kvp.Key;
419  if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
420  if (affliction.Prefab.AfflictionType == afflictionType)
421  {
422  strength += affliction.Strength;
423  }
424  }
425  }
426  return strength;
427  }
428 
429  public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true)
430  {
431  return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions);
432  }
433 
434  public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true)
435  {
436  return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions);
437  }
438 
439  public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true)
440  {
441  float strength = 0.0f;
442  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
443  {
444  if (!allowLimbAfflictions && kvp.Value != null) { continue; }
445  var affliction = kvp.Key;
446  if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
447  if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) &&
448  (affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty))
449  {
450  strength += affliction.Strength;
451  }
452  }
453  return strength;
454  }
455 
456  public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true)
457  {
458  if (Character.GodMode) { return; }
459  if (!ignoreUnkillability)
460  {
461  if (!affliction.Prefab.IsBuff && Unkillable) { return; }
462  }
463  if (affliction.Prefab.LimbSpecific)
464  {
465  if (targetLimb == null)
466  {
467  //if a limb-specific affliction is applied to no specific limb, apply to all limbs
468  foreach (LimbHealth limbHealth in limbHealths)
469  {
470  AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
471  }
472 
473  }
474  else
475  {
476  AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
477  }
478  }
479  else
480  {
481  AddAffliction(affliction, allowStacking: allowStacking);
482  }
483  }
484 
488  public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
489  {
490  // This is a % resistance (0 to 1.0)
491  float resistance = 0.0f;
492  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
493  {
494  var affliction = kvp.Key;
495  resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
496  }
497  // This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
498  float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
499  // The returned value is calculated to be a % resistance again
500  return 1 - ((1 - resistance) * abilityResistanceMultiplier);
501  }
502 
503  public float GetStatValue(StatTypes statType)
504  {
505  float value = 0f;
506  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
507  {
508  var affliction = kvp.Key;
509  value += affliction.GetStatValue(statType);
510  }
511  return value;
512  }
513 
514  public bool HasFlag(AbilityFlags flagType)
515  {
516  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
517  {
518  var affliction = kvp.Key;
519  if (affliction.HasFlag(flagType)) { return true; }
520  }
521  return false;
522  }
523 
524  private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
525 
526  public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
527  {
528  matchingAfflictions.Clear();
529  matchingAfflictions.AddRange(afflictions.Keys);
530 
531  ReduceMatchingAfflictions(amount, treatmentAction);
532  }
533 
534  public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
535  {
536  if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
537 
538  matchingAfflictions.Clear();
539  foreach (var affliction in afflictions)
540  {
541  if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
542  {
543  matchingAfflictions.Add(affliction.Key);
544  }
545  }
546 
547  ReduceMatchingAfflictions(amount, treatmentAction, attacker);
548  }
549 
550  private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
551  => afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]);
552 
553  public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null)
554  {
555  if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
556 
557  matchingAfflictions.Clear();
558  matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
559 
560  ReduceMatchingAfflictions(amount, treatmentAction);
561  }
562 
563  public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
564  {
565  if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
566  if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
567 
568  matchingAfflictions.Clear();
569  var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
570  foreach (var affliction in afflictions)
571  {
572  if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
573  affliction.Value == targetLimbHealth)
574  {
575  matchingAfflictions.Add(affliction.Key);
576  }
577  }
578  ReduceMatchingAfflictions(amount, treatmentAction, attacker);
579  }
580 
581  private void ReduceMatchingAfflictions(float amount, ActionType? treatmentAction, Character attacker = null)
582  {
583  if (matchingAfflictions.Count == 0) { return; }
584 
585  float reduceAmount = amount / matchingAfflictions.Count;
586 
587  if (reduceAmount > 0f)
588  {
589  var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount);
590  attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction);
591  reduceAmount = abilityReduceAffliction.Value;
592  }
593 
594  for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
595  {
596  var matchingAffliction = matchingAfflictions[i];
597 
598  if (matchingAffliction.Strength < reduceAmount)
599  {
600  float surplus = reduceAmount - matchingAffliction.Strength;
601  amount -= matchingAffliction.Strength;
602  matchingAffliction.Strength = 0.0f;
603  matchingAfflictions.RemoveAt(i);
604  if (i == 0) { i = matchingAfflictions.Count; }
605  if (i > 0) { reduceAmount += surplus / i; }
606  AchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
607  }
608  else
609  {
610  matchingAffliction.Strength -= reduceAmount;
611  amount -= reduceAmount;
612  if (treatmentAction != null)
613  {
614  if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
615  {
616  matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
617  }
618  else if (treatmentAction.Value == ActionType.OnFailure)
619  {
620  matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
621  }
622  }
623  }
624  }
625  CalculateVitality();
626  }
627 
632  public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true)
633  {
634  if (Unkillable || Character.GodMode) { return; }
635  if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
636  {
637  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
638  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
639  return;
640  }
641 
642  var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyDamage", this, attackResult, hitLimb, allowStacking);
643  if (should != null && should.Value) { return; }
644 
645  foreach (Affliction newAffliction in attackResult.Afflictions)
646  {
647  if (newAffliction.Prefab.LimbSpecific)
648  {
649  AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality);
650  }
651  else
652  {
653  // Always recalculate vitality for non-limb specific afflictions.
654  AddAffliction(newAffliction, allowStacking);
655  }
656  }
657  }
658 
659  private void KillIfOutOfVitality()
660  {
661  if (Vitality <= MinVitality &&
662  !Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
663  {
664  Kill();
665  }
666  }
667 
668  private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
669  private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
670  public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
671  {
672  if (Unkillable || Character.GodMode) { return; }
673 
674  afflictionsToRemove.Clear();
675  afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
676  a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
677  a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
678  a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
679  foreach (var affliction in afflictionsToRemove)
680  {
681  afflictions.Remove(affliction);
682  }
683 
684  foreach (LimbHealth limbHealth in limbHealths)
685  {
686  if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
687  if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
688  if (burnDamageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
689  }
690 
692  }
693 
694  public float GetLimbDamage(Limb limb, Identifier afflictionType)
695  {
696  float damageStrength;
697  if (limb.IsSevered)
698  {
699  return 1;
700  }
701  else
702  {
703  // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
704  // Therefore with e.g. 80 health, the max damage per limb would be 40.
705  // Having at least 40 damage on both legs would cause maximum limping.
706  float max = MaxVitality / 2;
707  if (afflictionType.IsEmpty)
708  {
709  float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
710  float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
711  float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
712  damageStrength = Math.Min(damage + bleeding + burn, max);
713  }
714  else
715  {
716  damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
717  }
718  return damageStrength / max;
719  }
720  }
721 
722  public void RemoveAfflictions(Func<Affliction, bool> predicate)
723  {
724  afflictionsToRemove.Clear();
725  afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
726  foreach (var affliction in afflictionsToRemove)
727  {
728  afflictions.Remove(affliction);
729  }
730  CalculateVitality();
731  }
732 
733  public void RemoveAllAfflictions()
734  {
735  afflictionsToRemove.Clear();
736  afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
737  foreach (var affliction in afflictionsToRemove)
738  {
739  afflictions.Remove(affliction);
740  }
741  foreach (Affliction affliction in irremovableAfflictions)
742  {
743  affliction.Strength = 0.0f;
744  }
745  CalculateVitality();
746  }
747 
749  {
750  afflictionsToRemove.Clear();
751  afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
752  !irremovableAfflictions.Contains(a) &&
753  !a.Prefab.IsBuff &&
754  a.Prefab.AfflictionType != "geneticmaterialbuff" &&
755  a.Prefab.AfflictionType != "geneticmaterialdebuff"));
756  foreach (var affliction in afflictionsToRemove)
757  {
758  afflictions.Remove(affliction);
759  }
760  foreach (Affliction affliction in irremovableAfflictions)
761  {
762  affliction.Strength = 0.0f;
763  }
764  CalculateVitality();
765  }
766 
771  private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
772  {
773  if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
774  if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
775  {
776  DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
777  "\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
778  return;
779  }
780  AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality);
781  }
782 
787  private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
788  {
789  LimbType limbType = limb?.type ?? LimbType.None;
790  if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
791  if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
792  if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
793  if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
794  {
795  if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
796  {
797  return;
798  }
799  }
801  {
802  if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
803  {
804  return;
805  }
806  }
807  if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
808  if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
809  if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
810 
811  var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyAffliction", this, limbHealth, newAffliction, allowStacking);
812 
813  if (should != null && should.Value)
814  return;
815 
816  Affliction existingAffliction = null;
817  foreach ((Affliction affliction, LimbHealth value) in afflictions)
818  {
819  if (value == limbHealth && affliction.Prefab == newAffliction.Prefab)
820  {
821  existingAffliction = affliction;
822  break;
823  }
824  }
825 
826  if (existingAffliction != null)
827  {
828  float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
829  if (allowStacking)
830  {
831  // Add the existing strength
832  newStrength += existingAffliction.Strength;
833  }
834  newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
835  if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
836  existingAffliction.Strength = newStrength;
837  existingAffliction.Duration = existingAffliction.Prefab.Duration;
838  if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
839  if (recalculateVitality)
840  {
842  }
843  return;
844  }
845 
846  //create a new instance of the affliction to make sure we don't use the same instance for multiple characters
847  //or modify the affliction instance of an Attack or a StatusEffect
848  var copyAffliction = newAffliction.Prefab.Instantiate(
849  Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
850  newAffliction.Source);
851  afflictions.Add(copyAffliction, limbHealth);
852  AchievementManager.OnAfflictionReceived(copyAffliction, Character);
853  MedicalClinic.OnAfflictionCountChanged(Character);
854 
856 
857  if (recalculateVitality)
858  {
860  }
861 #if CLIENT
862  if (OpenHealthWindow != this && limbHealth != null)
863  {
864  selectedLimbIndex = -1;
865  }
866 #endif
867  }
868 
869  private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
870  {
871  AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking);
872  }
873 
874  partial void UpdateSkinTint();
875 
876  partial void UpdateLimbAfflictionOverlays();
877 
878  public void Update(float deltaTime)
879  {
880  WasInFullHealth = vitality >= MaxVitality;
881 
882  UpdateOxygen(deltaTime);
883 
884  StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
885 
886  if (!Character.GodMode)
887  {
888  afflictionsToRemove.Clear();
889  afflictionsToUpdate.Clear();
890  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
891  {
892  var affliction = kvp.Key;
893  if (affliction.Strength <= 0.0f)
894  {
895  AchievementManager.OnAfflictionRemoved(affliction, Character);
896  if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
897  continue;
898  }
899  if (affliction.Prefab.Duration > 0.0f)
900  {
901  affliction.Duration -= deltaTime;
902  if (affliction.Duration <= 0.0f)
903  {
904  afflictionsToRemove.Add(affliction);
905  continue;
906  }
907  }
908  afflictionsToUpdate.Add(kvp);
909  }
910  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
911  {
912  var affliction = kvp.Key;
913  Limb targetLimb = null;
914  if (kvp.Value != null)
915  {
916  int healthIndex = limbHealths.IndexOf(kvp.Value);
917  targetLimb =
918  Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
920  }
921  affliction.Update(this, targetLimb, deltaTime);
922  affliction.DamagePerSecondTimer += deltaTime;
923  if (affliction is AfflictionBleeding bleeding)
924  {
925  UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
926  }
927  Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
928  }
929 
930  foreach (var affliction in afflictionsToRemove)
931  {
932  afflictions.Remove(affliction);
933  }
934 
935  if (afflictionsToRemove.Count is not 0)
936  {
937  MedicalClinic.OnAfflictionCountChanged(Character);
938  }
939  }
940 
942  if (Character.InWater)
943  {
945  }
946  else
947  {
949  }
950 
951  UpdateDamageReductions(deltaTime);
952 
953  if (!Character.GodMode)
954  {
955 #if CLIENT
956  updateVisualsTimer -= deltaTime;
957  if (Character.IsVisible && updateVisualsTimer <= 0.0f)
958  {
959  UpdateLimbAfflictionOverlays();
960  UpdateSkinTint();
961  updateVisualsTimer = UpdateVisualsInterval;
962  }
963 #endif
965  }
966  }
967 
968  public void ForceUpdateVisuals()
969  {
970  UpdateLimbAfflictionOverlays();
971  UpdateSkinTint();
972  }
973 
974  private void UpdateDamageReductions(float deltaTime)
975  {
977  if (healthRegen > 0)
978  {
979  ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), healthRegen * deltaTime);
980  }
981  float burnReduction = Character.Params.Health.BurnReduction;
982  if (burnReduction > 0)
983  {
984  ReduceAfflictionOnAllLimbs("burn".ToIdentifier(), burnReduction * deltaTime);
985  }
986  float bleedingReduction = Character.Params.Health.BleedingReduction;
987  if (bleedingReduction > 0)
988  {
989  ReduceAfflictionOnAllLimbs("bleeding".ToIdentifier(), bleedingReduction * deltaTime);
990  }
991  }
992 
996  public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
997 
998  private void UpdateOxygen(float deltaTime)
999  {
1000  if (!Character.NeedsOxygen)
1001  {
1002  oxygenLowAffliction.Strength = 0.0f;
1003  return;
1004  }
1005 
1006  float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
1007  float prevOxygen = OxygenAmount;
1008  if (IsUnconscious)
1009  {
1010  //clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
1011  float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
1012  //the character dies of oxygen deprivation in 100 seconds after losing consciousness
1013  OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
1014  }
1015  else
1016  {
1017  float decreaseSpeed = -5.0f;
1018  float increaseSpeed = 10.0f;
1019  decreaseSpeed *= (1f - oxygenlowResistance);
1020  increaseSpeed *= (1f + oxygenlowResistance);
1021  float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
1022  if (holdBreathMultiplier <= -1.0f)
1023  {
1024  OxygenAmount = -100.0f;
1025  }
1026  else
1027  {
1028  decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
1029  OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
1030  }
1031  }
1032 
1033  UpdateOxygenProjSpecific(prevOxygen, deltaTime);
1034  }
1035 
1036  partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
1037 
1038  partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
1039 
1040  public void SetVitality(float newVitality)
1041  {
1042  UnmodifiedMaxVitality = newVitality;
1043  CalculateVitality();
1044  }
1045 
1046  private void CalculateVitality()
1047  {
1048  vitality = MaxVitality;
1049  IsParalyzed = false;
1050  if (Unkillable || Character.GodMode) { return; }
1051 
1052  foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
1053  {
1054  float vitalityDecrease = affliction.GetVitalityDecrease(this);
1055  if (limbHealth != null)
1056  {
1057  vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
1058  }
1059  vitality -= vitalityDecrease;
1060  affliction.CalculateDamagePerSecond(vitalityDecrease);
1061 
1062  if (affliction.Strength >= affliction.Prefab.MaxStrength &&
1063  affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
1064  {
1065  IsParalyzed = true;
1066  }
1067  }
1068 #if CLIENT
1069  if (IsUnconscious)
1070  {
1071  HintManager.OnCharacterUnconscious(Character);
1072  }
1073 #endif
1074  }
1075 
1076  public void RecalculateVitality()
1077  {
1078  CalculateVitality();
1079  KillIfOutOfVitality();
1080  }
1081 
1082  private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
1083  {
1084  float multiplier = 1.0f;
1085  if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
1086  {
1087  multiplier *= vitalityMultiplier;
1088  }
1089  if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier))
1090  {
1091  multiplier *= vitalityTypeMultiplier;
1092  }
1093  return multiplier;
1094  }
1095 
1099  private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction)
1100  {
1101  float vitalityDecrease = affliction.GetVitalityDecrease(this);
1102  if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null)
1103  {
1104  vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
1105  }
1106  return vitalityDecrease;
1107  }
1108 
1109  private void Kill()
1110  {
1111  if (Unkillable || Character.GodMode) { return; }
1112 
1113  var (type, affliction) = GetCauseOfDeath();
1114  UpdateLimbAfflictionOverlays();
1115  UpdateSkinTint();
1116  Character.Kill(type, affliction);
1117 
1118  WasInFullHealth = false;
1119 #if CLIENT
1120  DisplayVitalityDelay = 0.0f;
1121  DisplayedVitality = Vitality;
1122 #endif
1123  }
1124 
1125  // We need to use another list of the afflictions when we call the status effects triggered by afflictions,
1126  // because those status effects may add or remove other afflictions while iterating the collection.
1127  private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
1129  {
1130  afflictionsCopy.Clear();
1131  afflictionsCopy.AddRange(afflictions.Keys);
1132  foreach (Affliction affliction in afflictionsCopy)
1133  {
1134  affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
1135  }
1136  }
1137 
1138  public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath()
1139  {
1140  List<Affliction> currentAfflictions = GetAllAfflictions(true);
1141 
1142  Affliction strongestAffliction = null;
1143  float largestStrength = 0.0f;
1144  foreach (Affliction affliction in currentAfflictions)
1145  {
1146  if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
1147  {
1148  strongestAffliction = affliction;
1149  largestStrength = affliction.GetVitalityDecrease(this);
1150  }
1151  }
1152 
1153  CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
1154  if (strongestAffliction == oxygenLowAffliction)
1155  {
1156  causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
1157  }
1158 
1159  return (causeOfDeath, strongestAffliction);
1160  }
1161 
1162  private readonly List<Affliction> allAfflictions = new List<Affliction>();
1163  private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
1164  {
1165  allAfflictions.Clear();
1166  if (!mergeSameAfflictions)
1167  {
1168  allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
1169  }
1170  else
1171  {
1172  foreach (Affliction affliction in afflictions.Keys)
1173  {
1174  if (predicate != null && !predicate(affliction)) { continue; }
1175  var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
1176  if (existingAffliction == null)
1177  {
1178  var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
1179  if (affliction.Source != null) { newAffliction.Source = affliction.Source; }
1180  newAffliction.DamagePerSecond = affliction.DamagePerSecond;
1181  newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
1182  allAfflictions.Add(newAffliction);
1183  }
1184  else
1185  {
1186  existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
1187  existingAffliction.Strength += affliction.Strength;
1188  }
1189  }
1190  }
1191  return allAfflictions;
1192  }
1193 
1200  public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false, float predictFutureDuration = 0.0f)
1201  {
1202  //key = item identifier
1203  //float = suitability
1204  treatmentSuitability.Clear();
1205  float minSuitability = -10, maxSuitability = 10;
1206  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1207  {
1208  var affliction = kvp.Key;
1209  var limbHealth = kvp.Value;
1210  if (limb != null &&
1211  affliction.Prefab.LimbSpecific &&
1212  GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb))
1213  {
1214  if (limbHealth == null) { continue; }
1215  int healthIndex = limbHealths.IndexOf(limbHealth);
1216  if (limb.HealthIndex != healthIndex) { continue; }
1217  }
1218 
1219  float strength = affliction.Strength;
1220  if (predictFutureDuration > 0.0f)
1221  {
1222  strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
1223  }
1224 
1225  //other afflictions of the same type increase the "treatability"
1226  // e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
1227  float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
1228 
1229  if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
1230 
1231  if (ignoreHiddenAfflictions)
1232  {
1233  if (user == Character)
1234  {
1235  if (strength < affliction.Prefab.ShowIconThreshold) { continue; }
1236  }
1237  else
1238  {
1239  if (strength < affliction.Prefab.ShowIconToOthersThreshold) { continue; }
1240  }
1241  }
1242 
1243  foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
1244  {
1245  float suitability = treatment.Value * strength;
1246  if (suitability > 0)
1247  {
1248  //if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
1249  //if the suitability is negative though, we need to take it into account!
1250  //otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
1251  if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
1252  }
1253  if (treatment.Value > strength)
1254  {
1255  //avoid using very effective meds on small injuries
1256  float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
1257  suitability /= overtreatmentFactor;
1258  }
1259  if (!treatmentSuitability.ContainsKey(treatment.Key))
1260  {
1261  treatmentSuitability[treatment.Key] = suitability;
1262  }
1263  else
1264  {
1265  treatmentSuitability[treatment.Key] += suitability;
1266  }
1267  minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
1268  maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
1269  }
1270  }
1271  }
1272 
1279  public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
1280  {
1281  float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
1282  if (affliction.Prefab.LimbSpecific)
1283  {
1284  foreach (Affliction otherAffliction in afflictions.Keys)
1285  {
1286  if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
1287  {
1288  totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
1289  }
1290  }
1291  }
1292  return totalAfflictionStrength;
1293  }
1294 
1295  private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
1296  public IEnumerable<Identifier> GetActiveAfflictionTags()
1297  {
1298  afflictionTags.Clear();
1299  foreach (Affliction affliction in afflictions.Keys)
1300  {
1301  var currentEffect = affliction.GetActiveEffect();
1302  if (currentEffect is { Tag.IsEmpty: false })
1303  {
1304  afflictionTags.Add(currentEffect.Tag);
1305  }
1306  }
1307  return afflictionTags;
1308  }
1309 
1310  public float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb = null)
1311  {
1312  float strength = affliction.Strength;
1313  foreach (var statusEffect in StatusEffect.DurationList)
1314  {
1315  if (!statusEffect.Targets.Any(t => t == Character || (limb != null && Character.AnimController.Limbs.Contains(t)))) { continue; }
1316  float statusEffectDuration = Math.Min(statusEffect.Timer, predictFutureDuration);
1317  foreach (var statusEffectAffliction in statusEffect.Parent.Afflictions)
1318  {
1319  if (statusEffectAffliction.Prefab == affliction.Prefab)
1320  {
1321  strength += statusEffectAffliction.Strength * statusEffectDuration;
1322  }
1323  }
1324  foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction)
1325  {
1326  if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier ||
1327  statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType)
1328  {
1329  strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration;
1330  }
1331  }
1332  }
1333  return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
1334  }
1335 
1336  private readonly List<Affliction> activeAfflictions = new List<Affliction>();
1337  private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
1338  public void ServerWrite(IWriteMessage msg)
1339  {
1340  activeAfflictions.Clear();
1341  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1342  {
1343  var affliction = kvp.Key;
1344  var limbHealth = kvp.Value;
1345  if (limbHealth != null) { continue; }
1346  if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
1347  {
1348  activeAfflictions.Add(affliction);
1349  }
1350  }
1351  msg.WriteByte((byte)activeAfflictions.Count);
1352  foreach (Affliction affliction in activeAfflictions)
1353  {
1354  msg.WriteUInt32(affliction.Prefab.UintIdentifier);
1355  msg.WriteRangedSingle(
1356  MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
1357  0.0f, affliction.Prefab.MaxStrength, 8);
1358  msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
1359  foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
1360  {
1361  msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], 0, periodicEffect.MaxInterval, 8);
1362  }
1363  }
1364 
1365  limbAfflictions.Clear();
1366  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1367  {
1368  var limbAffliction = kvp.Key;
1369  var limbHealth = kvp.Value;
1370  if (limbHealth == null) { continue; }
1371  if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) { continue; }
1372  limbAfflictions.Add((limbHealth, limbAffliction));
1373  }
1374 
1375  msg.WriteByte((byte)limbAfflictions.Count);
1376  foreach (var (limbHealth, affliction) in limbAfflictions)
1377  {
1378  msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1);
1379  msg.WriteUInt32(affliction.Prefab.UintIdentifier);
1380  msg.WriteRangedSingle(
1381  MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
1382  0.0f, affliction.Prefab.MaxStrength, 8);
1383  msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
1384  foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
1385  {
1386  msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
1387  }
1388  }
1389  }
1390 
1391  public void Remove()
1392  {
1393  RemoveProjSpecific();
1394  afflictionsToRemove.Clear();
1395  afflictionsToUpdate.Clear();
1396  }
1397 
1398  partial void RemoveProjSpecific();
1399 
1403  public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
1404  afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength);
1405 
1406  public void Save(XElement healthElement)
1407  {
1408  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
1409  {
1410  var affliction = kvp.Key;
1411  var limbHealth = kvp.Value;
1412  if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
1413  if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
1414  healthElement.Add(new XElement("Affliction",
1415  new XAttribute("identifier", affliction.Identifier),
1416  new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
1417  }
1418 
1419  for (int i = 0; i < limbHealths.Count; i++)
1420  {
1421  var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
1422  healthElement.Add(limbHealthElement);
1423  foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions.Where(a => a.Value == limbHealths[i]))
1424  {
1425  var affliction = kvp.Key;
1426  var limbHealth = kvp.Value;
1427  if (affliction.Strength <= 0.0f) { continue; }
1428  limbHealthElement.Add(new XElement("Affliction",
1429  new XAttribute("identifier", affliction.Identifier),
1430  new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
1431  }
1432  }
1433  }
1434 
1435  public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
1436  {
1437  foreach (var subElement in element.Elements())
1438  {
1439  switch (subElement.Name.ToString().ToLowerInvariant())
1440  {
1441  case "affliction":
1442  LoadAffliction(subElement);
1443  break;
1444  case "limbhealth":
1445  int limbHealthIndex = subElement.GetAttributeInt("i", -1);
1446  if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
1447  {
1448  DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
1449  continue;
1450  }
1451  foreach (XElement afflictionElement in subElement.Elements())
1452  {
1453  LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
1454  }
1455  break;
1456  }
1457  }
1458 
1459  void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
1460  {
1461  string id = afflictionElement.GetAttributeString("identifier", "");
1462  var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
1463  if (afflictionPrefab == null)
1464  {
1465  DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
1466  return;
1467  }
1468  if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
1469  float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
1470  var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
1471  if (irremovableAffliction != null)
1472  {
1473  irremovableAffliction.Strength = strength;
1474  }
1475  else
1476  {
1477  afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
1478  }
1479  }
1480  }
1481  }
1482 }
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 GetStatValue(StatTypes statType)
Definition: Affliction.cs:375
virtual float Strength
Definition: Affliction.cs:31
AfflictionPrefab.Effect GetActiveEffect()
Definition: Affliction.cs:160
float GetResistance(Identifier afflictionId, LimbType limbType)
How much resistance to the specified affliction does this affliction currently give?
Definition: Affliction.cs:343
readonly AfflictionPrefab Prefab
Definition: Affliction.cs:12
void ApplyStatusEffects(ActionType type, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
Definition: Affliction.cs:467
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
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
readonly Dictionary< Identifier, float > VitalityTypeMultipliers
readonly Dictionary< Identifier, float > VitalityMultipliers
LimbHealth(ContentXElement element, CharacterHealth characterHealth)
void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking=true, bool ignoreUnkillability=false, bool recalculateVitality=true)
readonly Character Character
float VitalityDisregardingDeath
How much vitality the character would have if it was alive? E.g. a character killed by disconnection ...
float GetStatValue(StatTypes statType)
float GetLimbDamage(Limb limb, Identifier afflictionType)
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 ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking=true, bool recalculateVitality=true)
bool ShowDamageOverlay
Show the blood overlay screen space effect when the character takes damage. Enabled normally,...
void SetVitality(float newVitality)
void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction=null, Character attacker=null)
float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
How much resistance all the afflictions the character has give to the specified affliction?
bool HasFlag(AbilityFlags flagType)
static IEnumerable< Affliction > SortAfflictionsBySeverity(IEnumerable< Affliction > afflictions, bool excludeBuffs=true)
Automatically filters out buffs.
void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction=null)
CharacterHealth(Character character)
Limb GetAfflictionLimb(Affliction affliction)
IEnumerable< Affliction > GetAllAfflictions(Func< Affliction, bool > limbHealthFilter)
void Update(float deltaTime)
const float InsufficientOxygenThreshold
void ServerWrite(IWriteMessage msg)
void Load(XElement element, Func< AfflictionPrefab, bool > afflictionPredicate=null)
IEnumerable< Identifier > GetActiveAfflictionTags()
IReadOnlyCollection< Affliction > GetAllAfflictions()
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 ...
static readonly Color DefaultFaceTint
Affliction GetAffliction(Identifier identifier, Limb limb)
void RemoveAfflictions(Func< Affliction, bool > predicate)
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,...
float UnmodifiedMaxVitality
Maximum vitality without talent- or job-based modifiers
float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions=true)
void Save(XElement healthElement)
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)
void ApplyAfflictionStatusEffects(ActionType type)
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)
IEnumerable< Identifier > ImmunityIdentifiers
void Add(ContentXElement elem)
string? GetAttributeString(string key, string? def)
float GetAttributeFloat(string key, float def)
static LuaCsSetup LuaCs
Definition: GameMain.cs:37
static GameSession GameSession
Definition: GameMain.cs:45
CampaignMode? Campaign
Definition: GameSession.cs:128
JobPrefab Prefab
Definition: Job.cs:18
void Update(float deltaTime)
Definition: Limb.cs:922
readonly LimbType type
Definition: Limb.cs:227
bool? IsSevered
Definition: Limb.cs:351
int HealthIndex
Definition: Limb.cs:275
object Call(string name, params object[] args)
readonly Identifier Identifier
Definition: Prefab.cs:34
Limb GetLimb(LimbType limbType, bool excludeSevered=true, bool excludeLimbsWithSecondaryType=false, bool useSecondaryType=false)
Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
Definition: Ragdoll.cs:2130
Vector2 size
Definition: Sprite.cs:36
StatusEffects can be used to execute various kinds of effects: modifying the state of some entity in ...
Definition: StatusEffect.cs:72
static readonly List< DurationListElement > DurationList
LimbType
Definition: Limb.cs:19
AbilityFlags
AbilityFlags are a set of toggleable flags that can be applied to characters.
Definition: Enums.cs:641
ActionType
ActionTypes define when a StatusEffect is executed.
Definition: Enums.cs:26
AbilityEffectType
Definition: Enums.cs:140
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:195