Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
T
Eero 45312af297 WIP Make static collections thread-safe using ThreadStatic and ThreadLocal
Refactored various static and instance collections to use [ThreadStatic], ThreadLocal, or local variables to prevent concurrent modification issues during parallel updates. This affects status effect targets, affliction lists, damage modifiers, and cached data in Character, CharacterHealth, Limb, Explosion, Hull, Submarine, and ToolBox classes. Also replaced Dictionary caches with ConcurrentDictionary where appropriate for thread safety.
2025-12-28 14:14:53 +08:00

1566 lines
70 KiB
C#

using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using MoonSharp.Interpreter;
namespace Barotrauma
{
partial class CharacterHealth
{
public class LimbHealth
{
public Sprite IndicatorSprite;
public Sprite HighlightSprite;
public Rectangle HighlightArea;
public readonly LocalizedString Name;
//public readonly List<Affliction> Afflictions = new List<Affliction>();
public readonly Dictionary<Identifier, float> VitalityMultipliers = new Dictionary<Identifier, float>();
public readonly Dictionary<Identifier, float> VitalityTypeMultipliers = new Dictionary<Identifier, float>();
public LimbHealth() { }
public LimbHealth(ContentXElement element, CharacterHealth characterHealth)
{
string limbName = element.GetAttributeString("name", null) ?? "generic";
if (limbName != "generic")
{
Name = TextManager.Get("HealthLimbName." + limbName);
}
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "sprite":
IndicatorSprite = new Sprite(subElement);
HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
break;
case "highlightsprite":
HighlightSprite = new Sprite(subElement);
break;
case "vitalitymultiplier":
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.",
contentPackage: element.ContentPackage);
continue;
}
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
if (vitalityMultipliers != null)
{
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
foreach (var vitalityMultiplier in vitalityMultipliers)
{
VitalityMultipliers.Add(vitalityMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier))
{
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?",
contentPackage: element.ContentPackage);
}
}
}
var vitalityTypeMultipliers = subElement.GetAttributeIdentifierArray("type", null) ?? subElement.GetAttributeIdentifierArray("types", null);
if (vitalityTypeMultipliers != null)
{
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
foreach (var vitalityTypeMultiplier in vitalityTypeMultipliers)
{
VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier))
{
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?",
contentPackage: element.ContentPackage);
}
}
}
if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
{
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!",
contentPackage: element.ContentPackage);
}
break;
}
}
}
}
public const float InsufficientOxygenThreshold = 30.0f;
public const float LowOxygenThreshold = 50.0f;
protected float minVitality;
/// <summary>
/// Maximum vitality without talent- or job-based modifiers
/// </summary>
protected float UnmodifiedMaxVitality
{
get => Character.Params.Health.Vitality;
set => Character.Params.Health.Vitality = value;
}
public bool Unkillable;
public bool DoesBleed
{
get => Character.Params.Health.DoesBleed && !Character.Params.IsMachine;
private set => Character.Params.Health.DoesBleed = value;
}
public bool UseHealthWindow
{
get => Character.Params.Health.UseHealthWindow;
set => Character.Params.Health.UseHealthWindow = value;
}
public float CrushDepth
{
get => Character.Params.Health.CrushDepth;
private set => Character.Params.Health.CrushDepth = value;
}
private readonly List<LimbHealth> limbHealths = new List<LimbHealth>();
// Thread-safe afflictions dictionary for concurrent access
private readonly ConcurrentDictionary<Affliction, LimbHealth> afflictions = new ConcurrentDictionary<Affliction, LimbHealth>();
private readonly ConcurrentDictionary<Affliction, byte> irremovableAfflictions = new ConcurrentDictionary<Affliction, byte>();
private Affliction bloodlossAffliction;
private Affliction oxygenLowAffliction;
private Affliction pressureAffliction;
private Affliction stunAffliction;
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }
}
public float PressureKillDelay { get; private set; } = 5.0f;
private float vitality;
public float Vitality
{
get
{
if (Character.IsDead)
{
return minVitality;
}
return vitality;
}
}
/// <summary>
/// How much vitality the character would have if it was alive?
/// E.g. a character killed by disconnection or with console commands may not have any vitality-reducing afflictions despite being dead
/// </summary>
public float VitalityDisregardingDeath => vitality;
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
public float MaxVitality
{
get
{
float max = UnmodifiedMaxVitality;
if (Character?.Info?.Job?.Prefab != null)
{
max += Character.Info.Job.Prefab.VitalityModifier;
}
max *= Character.HumanPrefabHealthMultiplier;
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
max *= Character.IsOnPlayerTeam
? campaign.Settings.CrewVitalityMultiplier
: campaign.Settings.NonCrewVitalityMultiplier;
}
max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
return max * Character.HealthMultiplier;
}
}
public float MinVitality
{
get
{
if (Character?.Info?.Job?.Prefab != null)
{
return -MaxVitality;
}
return minVitality;
}
}
public static readonly Color DefaultFaceTint = Color.TransparentBlack;
public Color FaceTint
{
get;
private set;
}
public Color BodyTint
{
get;
private set;
}
public float OxygenAmount
{
get
{
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; }
return -oxygenLowAffliction.Strength + 100;
}
set
{
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; }
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
}
}
public float BloodlossAmount
{
get { return bloodlossAffliction.Strength; }
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); }
}
public float Stun
{
get { return stunAffliction.Strength; }
set
{
if (Character.GodMode) { return; }
stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength);
}
}
public bool IsParalyzed { get; private set; }
public float StunTimer { get; private set; }
/// <summary>
/// Was the character in full health at the beginning of the frame?
/// </summary>
public bool WasInFullHealth { get; private set; }
/// <summary>
/// Show the blood overlay screen space effect when the character takes damage.
/// Enabled normally, but can be disabled for some special cases.
/// </summary>
public bool ShowDamageOverlay = true;
public Affliction PressureAffliction
{
get { return pressureAffliction; }
}
public readonly Character Character;
public CharacterHealth(Character character)
{
this.Character = character;
vitality = 100.0f;
DoesBleed = true;
UseHealthWindow = false;
InitIrremovableAfflictions();
limbHealths.Add(new LimbHealth());
InitProjSpecific(null, character);
}
public CharacterHealth(ContentXElement element, Character character, ContentXElement limbHealthElement = null)
{
this.Character = character;
InitIrremovableAfflictions();
vitality = UnmodifiedMaxVitality;
minVitality = element.GetAttributeFloat(nameof(MinVitality), character.IsHuman ? -100.0f : 0.0f);
limbHealths.Clear();
limbHealthElement ??= element;
foreach (var subElement in limbHealthElement.Elements())
{
if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; }
limbHealths.Add(new LimbHealth(subElement, this));
}
if (limbHealths.Count == 0)
{
limbHealths.Add(new LimbHealth());
}
InitProjSpecific(element, character);
}
public void CheckForErrors()
{
for (int i = 0; i < limbHealths.Count; i++)
{
if (Character.AnimController.Limbs.None(l => l.HealthIndex == i))
{
DebugConsole.AddWarning(
$"Potential error in character \"{Character.Prefab.Identifier}\": none of the limbs have been set to use the LimbHealth #{i}, and it will do nothing. "
+ "Did you forget to set the HealthIndex values of the limbs?", contentPackage: Character.ContentPackage);
}
}
}
private void InitIrremovableAfflictions()
{
irremovableAfflictions.TryAdd(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f), 0);
irremovableAfflictions.TryAdd(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f), 0);
irremovableAfflictions.TryAdd(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f), 0);
irremovableAfflictions.TryAdd(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f), 0);
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
afflictions.TryAdd(affliction, null);
}
}
partial void InitProjSpecific(ContentXElement element, Character character);
public IReadOnlyCollection<Affliction> GetAllAfflictions()
{
return afflictions.Keys.ToList();
}
public IEnumerable<Affliction> GetAllAfflictions(Func<Affliction, bool> limbHealthFilter)
{
return afflictions.Keys.Where(limbHealthFilter);
}
private float GetTotalDamage(LimbHealth limbHealth)
{
float totalDamage = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
if (kvp.Value != limbHealth) { continue; }
var affliction = kvp.Key;
totalDamage += affliction.GetVitalityDecrease(this);
}
return totalDamage;
}
private LimbHealth GetMatchingLimbHealth(Limb limb) => limb == null ? null : limbHealths[limb.HealthIndex];
private LimbHealth GetMatchingLimbHealth(Affliction affliction) => GetMatchingLimbHealth(Character.AnimController.GetLimb(affliction.Prefab.IndicatorLimb, excludeSevered: false));
public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) =>
GetAffliction(identifier.ToIdentifier(), allowLimbAfflictions);
public Affliction GetAffliction(Identifier identifier, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions);
public Affliction GetAfflictionOfType(Identifier afflictionType, bool allowLimbAfflictions = true)
=> GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions);
private Affliction GetAffliction(Func<Affliction, bool> predicate, bool allowLimbAfflictions = true)
{
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
if (!allowLimbAfflictions && kvp.Value != null) { continue; }
if (predicate(kvp.Key)) { return kvp.Key; }
}
return null;
}
public T GetAffliction<T>(Identifier identifier, bool allowLimbAfflictions = true) where T : Affliction
{
return GetAffliction(identifier, allowLimbAfflictions) as T;
}
public Affliction GetAffliction(Identifier identifier, Limb limb)
{
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return null;
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
if (limbHealths[limb.HealthIndex] == kvp.Value && kvp.Key.Prefab.Identifier == identifier) { return kvp.Key; }
}
return null;
}
public Limb GetAfflictionLimb(Affliction affliction)
{
if (affliction != null && afflictions.TryGetValue(affliction, out LimbHealth limbHealth))
{
if (limbHealth == null) { return null; }
int limbHealthIndex = limbHealths.IndexOf(limbHealth);
foreach (Limb limb in Character.AnimController.Limbs)
{
if (limb.HealthIndex == limbHealthIndex) { return limb; }
}
}
return null;
}
/// <summary>
/// Get the total strength of the afflictions of a specific type attached to a specific limb
/// </summary>
/// <param name="afflictionType">Type of the affliction</param>
/// <param name="limb">The limb the affliction is attached to</param>
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
public float GetAfflictionStrength(Identifier afflictionType, Limb limb, bool requireLimbSpecific)
{
if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; }
float strength = 0.0f;
LimbHealth limbHealth = limbHealths[limb.HealthIndex];
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
if (kvp.Value == limbHealth)
{
Affliction affliction = kvp.Key;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if (affliction.Prefab.AfflictionType == afflictionType)
{
strength += affliction.Strength;
}
}
}
return strength;
}
public float GetAfflictionStrengthByType(Identifier afflictionType, bool allowLimbAfflictions = true)
{
return GetAfflictionStrength(afflictionType, afflictionidentifier: Identifier.Empty, allowLimbAfflictions);
}
public float GetAfflictionStrengthByIdentifier(Identifier afflictionIdentifier, bool allowLimbAfflictions = true)
{
return GetAfflictionStrength(afflictionType: Identifier.Empty, afflictionIdentifier, allowLimbAfflictions);
}
public float GetAfflictionStrength(Identifier afflictionType, Identifier afflictionidentifier, bool allowLimbAfflictions = true)
{
float strength = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
if (!allowLimbAfflictions && kvp.Value != null) { continue; }
var affliction = kvp.Key;
if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; }
if ((affliction.Prefab.AfflictionType == afflictionType || afflictionType.IsEmpty) &&
(affliction.Prefab.Identifier == afflictionidentifier || afflictionidentifier.IsEmpty))
{
strength += affliction.Strength;
}
}
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false, bool recalculateVitality = true)
{
if (Character.GodMode) { return; }
if (!ignoreUnkillability)
{
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
}
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
{
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
foreach (LimbHealth limbHealth in limbHealths)
{
AddLimbAffliction(limbHealth, limb: null, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
}
}
else
{
AddLimbAffliction(targetLimb, affliction, allowStacking: allowStacking, recalculateVitality: recalculateVitality);
}
}
else
{
AddAffliction(affliction, allowStacking: allowStacking);
}
}
/// <summary>
/// How much resistance all the afflictions the character has give to the specified affliction?
/// </summary>
public float GetResistance(AfflictionPrefab afflictionPrefab, LimbType limbType)
{
// ConcurrentDictionary is thread-safe, no lock needed
// This is a % resistance (0 to 1.0)
float resistance = 0.0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier, limbType);
}
// This is a multiplier, ie. 0.0 = 100% resistance and 1.0 = 0% resistance
float abilityResistanceMultiplier = Character.GetAbilityResistance(afflictionPrefab);
// The returned value is calculated to be a % resistance again
return 1 - ((1 - resistance) * abilityResistanceMultiplier);
}
public float GetStatValue(StatTypes statType)
{
float value = 0f;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
value += affliction.GetStatValue(statType);
}
return value;
}
public bool HasFlag(AbilityFlags flagType)
{
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
if (affliction.HasFlag(flagType)) { return true; }
}
return false;
}
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_matchingAfflictions;
private static List<Affliction> MatchingAfflictions => t_matchingAfflictions ??= new List<Affliction>();
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnAllLimbs(Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
foreach (var affliction in afflictions)
{
if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private IEnumerable<Affliction> GetAfflictionsForLimb(Limb targetLimb)
=> afflictions.Keys.Where(k => afflictions[k] == limbHealths[targetLimb.HealthIndex]);
public void ReduceAllAfflictionsOnLimb(Limb targetLimb, float amount, ActionType? treatmentAction = null)
{
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction);
}
public void ReduceAfflictionOnLimb(Limb targetLimb, Identifier afflictionIdOrType, float amount, ActionType? treatmentAction = null, Character attacker = null)
{
if (afflictionIdOrType.IsEmpty) { throw new ArgumentException($"{nameof(afflictionIdOrType)} is empty"); }
if (targetLimb is null) { throw new ArgumentNullException(nameof(targetLimb)); }
var matchingAfflictions = MatchingAfflictions;
matchingAfflictions.Clear();
var targetLimbHealth = limbHealths[targetLimb.HealthIndex];
foreach (var affliction in afflictions)
{
if ((affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType) &&
affliction.Value == targetLimbHealth)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(matchingAfflictions, amount, treatmentAction, attacker);
}
private void ReduceMatchingAfflictions(List<Affliction> matchingAfflictions, float amount, ActionType? treatmentAction, Character attacker = null)
{
if (matchingAfflictions.Count == 0) { return; }
float reduceAmount = amount / matchingAfflictions.Count;
if (reduceAmount > 0f)
{
var abilityReduceAffliction = new AbilityReduceAffliction(Character, reduceAmount);
attacker?.CheckTalents(AbilityEffectType.OnReduceAffliction, abilityReduceAffliction);
reduceAmount = abilityReduceAffliction.Value;
}
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
{
var matchingAffliction = matchingAfflictions[i];
if (matchingAffliction.Strength < reduceAmount)
{
float surplus = reduceAmount - matchingAffliction.Strength;
amount -= matchingAffliction.Strength;
matchingAffliction.Strength = 0.0f;
matchingAfflictions.RemoveAt(i);
if (i == 0) { i = matchingAfflictions.Count; }
if (i > 0) { reduceAmount += surplus / i; }
AchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
}
else
{
matchingAffliction.Strength -= reduceAmount;
amount -= reduceAmount;
if (treatmentAction != null)
{
if (treatmentAction.Value == ActionType.OnUse || treatmentAction.Value == ActionType.OnSuccess)
{
matchingAffliction.AppliedAsSuccessfulTreatmentTime = Timing.TotalTime;
}
else if (treatmentAction.Value == ActionType.OnFailure)
{
matchingAffliction.AppliedAsFailedTreatmentTime = Timing.TotalTime;
}
}
}
}
CalculateVitality();
}
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/>. Only applies to limb specific afflictions.</param>
public void ApplyDamage(Limb hitLimb, AttackResult attackResult, bool allowStacking = true, bool recalculateVitality = true)
{
if (Unkillable || Character.GodMode) { return; }
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
return;
}
var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyDamage", this, attackResult, hitLimb, allowStacking);
if (should != null && should.Value) { return; }
foreach (Affliction newAffliction in attackResult.Afflictions)
{
if (newAffliction.Prefab.LimbSpecific)
{
AddLimbAffliction(hitLimb, newAffliction, allowStacking, recalculateVitality: recalculateVitality);
}
else
{
// Always recalculate vitality for non-limb specific afflictions.
AddAffliction(newAffliction, allowStacking);
}
}
}
private void KillIfOutOfVitality()
{
if (Vitality <= MinVitality &&
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
Kill();
}
}
// Thread-static to avoid concurrent modification when multiple characters are updated in parallel
[ThreadStatic]
private static List<Affliction> t_afflictionsToRemove;
[ThreadStatic]
private static List<KeyValuePair<Affliction, LimbHealth>> t_afflictionsToUpdate;
private static List<Affliction> AfflictionsToRemove => t_afflictionsToRemove ??= new List<Affliction>();
private static List<KeyValuePair<Affliction, LimbHealth>> AfflictionsToUpdate => t_afflictionsToUpdate ??= new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable || Character.GodMode) { return; }
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType));
foreach (var affliction in afflictionsToRemove)
{
afflictions.TryRemove(affliction, out _);
}
foreach (LimbHealth limbHealth in limbHealths)
{
if (damageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.TryAdd(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.TryAdd(AfflictionPrefab.Burn.Instantiate(burnDamageAmount), limbHealth); }
}
RecalculateVitality();
}
public float GetLimbDamage(Limb limb, Identifier afflictionType)
{
float damageStrength;
if (limb.IsSevered)
{
return 1;
}
else
{
// Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value.
// Therefore with e.g. 80 health, the max damage per limb would be 40.
// Having at least 40 damage on both legs would cause maximum limping.
float max = MaxVitality / 2;
if (afflictionType.IsEmpty)
{
float damage = GetAfflictionStrength(AfflictionPrefab.DamageType, limb, true);
float bleeding = GetAfflictionStrength(AfflictionPrefab.BleedingType, limb, true);
float burn = GetAfflictionStrength(AfflictionPrefab.BurnType, limb, true);
damageStrength = Math.Min(damage + bleeding + burn, max);
}
else
{
damageStrength = Math.Min(GetAfflictionStrength(afflictionType, limb, true), max);
}
return damageStrength / max;
}
}
public void RemoveAfflictions(Func<Affliction, bool> predicate)
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
{
afflictions.TryRemove(affliction, out _);
}
CalculateVitality();
}
public void RemoveAllAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.ContainsKey(a)));
foreach (var affliction in afflictionsToRemove)
{
//set strength to 0 in case the affliction needs to react to becoming inactive
affliction.Strength = 0.0f;
afflictions.TryRemove(affliction, out _);
}
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
CalculateVitality();
}
public void RemoveNegativeAfflictions()
{
var afflictionsToRemove = AfflictionsToRemove;
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
!irremovableAfflictions.ContainsKey(a) &&
!a.Prefab.IsBuff &&
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
foreach (var affliction in afflictionsToRemove)
{
afflictions.TryRemove(affliction, out _);
}
foreach (Affliction affliction in irremovableAfflictions.Keys)
{
affliction.Strength = 0.0f;
}
CalculateVitality();
}
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
private void AddLimbAffliction(Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
{
if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; }
if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + limb.type + " is targeting index " + limb.HealthIndex);
return;
}
AddLimbAffliction(limbHealths[limb.HealthIndex], limb, newAffliction, allowStacking, recalculateVitality);
}
/// <summary>
///
/// </summary>
/// <param name="recalculateVitality">Set false only as an optimization when you manually call <see cref="RecalculateVitality"/></param>
private void AddLimbAffliction(LimbHealth limbHealth, Limb limb, Affliction newAffliction, bool allowStacking = true, bool recalculateVitality = true)
{
LimbType limbType = limb?.type ?? LimbType.None;
if (Character.Params.IsMachine && !newAffliction.Prefab.AffectMachines) { return; }
if (!DoesBleed && newAffliction is AfflictionBleeding) { return; }
if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; }
if (Character.Params.Health.StunImmunity && newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
if (Character.EmpVulnerability <= 0 || GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
{
return;
}
}
if (Character.Params.Health.PoisonImmunity)
{
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.PoisonType || newAffliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
return;
}
}
if (Character.EmpVulnerability <= 0 && newAffliction.Prefab.AfflictionType == AfflictionPrefab.EMPType) { return; }
if (newAffliction.Prefab.TargetSpecies.Any() && newAffliction.Prefab.TargetSpecies.None(s => s == Character.SpeciesName)) { return; }
if (Character.Params.Health.ImmunityIdentifiers.Contains(newAffliction.Identifier)) { return; }
var should = GameMain.LuaCs.Hook.Call<bool?>("character.applyAffliction", this, limbHealth, newAffliction, allowStacking);
if (should != null && should.Value)
return;
Affliction existingAffliction = null;
foreach ((Affliction affliction, LimbHealth value) in afflictions)
{
if (value == limbHealth && affliction.Prefab == newAffliction.Prefab)
{
existingAffliction = affliction;
break;
}
}
if (existingAffliction != null)
{
float newStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(existingAffliction.Prefab, limbType));
if (allowStacking)
{
// Add the existing strength
newStrength += existingAffliction.Strength;
}
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
existingAffliction.Strength = newStrength;
//set stun after setting the strength, because stun multipliers might want to set the strength to something else
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, allowStunDecrease: true, isNetworkMessage: true); }
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
if (recalculateVitality)
{
RecalculateVitality();
}
return;
}
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType))),
newAffliction.Source);
afflictions.TryAdd(copyAffliction, limbHealth);
AchievementManager.OnAfflictionReceived(copyAffliction, Character);
MedicalClinic.OnAfflictionCountChanged(Character);
Character.HealthUpdateInterval = 0.0f;
if (recalculateVitality)
{
RecalculateVitality();
}
#if CLIENT
if (OpenHealthWindow != this && limbHealth != null)
{
selectedLimbIndex = -1;
}
#endif
}
private void AddAffliction(Affliction newAffliction, bool allowStacking = true)
{
AddLimbAffliction(limbHealth: null, limb: null, newAffliction, allowStacking);
}
partial void UpdateSkinTint();
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
{
WasInFullHealth = vitality >= MaxVitality;
UpdateOxygen(deltaTime);
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
if (!Character.GodMode)
{
var afflictionsToRemove = AfflictionsToRemove;
var afflictionsToUpdate = AfflictionsToUpdate;
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
if (affliction.Strength <= 0.0f)
{
AchievementManager.OnAfflictionRemoved(affliction, Character);
if (!irremovableAfflictions.ContainsKey(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
if (affliction.Prefab.Duration > 0.0f)
{
affliction.Duration -= deltaTime;
if (affliction.Duration <= 0.0f)
{
//set strength to 0 in case the affliction needs to react to becoming inactive
affliction.Strength = 0.0f;
afflictionsToRemove.Add(affliction);
continue;
}
}
afflictionsToUpdate.Add(kvp);
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
{
var affliction = kvp.Key;
Limb targetLimb = null;
if (kvp.Value != null)
{
int healthIndex = limbHealths.IndexOf(kvp.Value);
targetLimb =
Character.AnimController.Limbs.LastOrDefault(l => !l.IsSevered && !l.Hidden && l.HealthIndex == healthIndex) ??
Character.AnimController.MainLimb;
}
affliction.Update(this, targetLimb, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
if (affliction is AfflictionBleeding bleeding)
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
foreach (var affliction in afflictionsToRemove)
{
afflictions.TryRemove(affliction, out _);
}
if (afflictionsToRemove.Count is not 0)
{
MedicalClinic.OnAfflictionCountChanged(Character);
}
}
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed));
if (Character.InWater)
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed));
}
else
{
Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.WalkingSpeed));
}
UpdateDamageReductions(deltaTime);
if (!Character.GodMode)
{
#if CLIENT
updateVisualsTimer -= deltaTime;
if (Character.IsVisible && updateVisualsTimer <= 0.0f)
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
updateVisualsTimer = UpdateVisualsInterval;
}
#endif
RecalculateVitality();
}
}
public void ForceUpdateVisuals()
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
}
private void UpdateDamageReductions(float deltaTime)
{
float healthRegen = Character.Params.Health.ConstantHealthRegeneration;
if (healthRegen > 0)
{
ReduceAfflictionOnAllLimbs("damage".ToIdentifier(), healthRegen * deltaTime);
}
float burnReduction = Character.Params.Health.BurnReduction;
if (burnReduction > 0)
{
ReduceAfflictionOnAllLimbs("burn".ToIdentifier(), burnReduction * deltaTime);
}
float bleedingReduction = Character.Params.Health.BleedingReduction;
if (bleedingReduction > 0)
{
ReduceAfflictionOnAllLimbs("bleeding".ToIdentifier(), bleedingReduction * deltaTime);
}
}
/// <summary>
/// 0-1.
/// </summary>
public float OxygenLowResistance => !Character.NeedsOxygen ? 1 : GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen)
{
oxygenLowAffliction.Strength = 0.0f;
return;
}
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab, LimbType.None);
float prevOxygen = OxygenAmount;
if (IsUnconscious)
{
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
}
else
{
float decreaseSpeed = -5.0f;
float increaseSpeed = 10.0f;
decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
if (holdBreathMultiplier <= -1.0f)
{
OxygenAmount = -100.0f;
}
else
{
decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
}
UpdateOxygenProjSpecific(prevOxygen, deltaTime);
}
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
public void SetVitality(float newVitality)
{
UnmodifiedMaxVitality = newVitality;
CalculateVitality();
}
private void CalculateVitality()
{
vitality = MaxVitality;
IsParalyzed = false;
if (Unkillable || Character.GodMode) { return; }
foreach ((Affliction affliction, LimbHealth limbHealth) in afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (limbHealth != null)
{
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
}
vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
if (affliction.Strength >= affliction.Prefab.MaxStrength &&
affliction.Prefab.AfflictionType == AfflictionPrefab.ParalysisType)
{
IsParalyzed = true;
}
}
#if CLIENT
if (IsUnconscious)
{
HintManager.OnCharacterUnconscious(Character);
}
#endif
}
public void RecalculateVitality()
{
CalculateVitality();
KillIfOutOfVitality();
}
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
float multiplier = 1.0f;
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
{
multiplier *= vitalityMultiplier;
}
if (limbHealth.VitalityTypeMultipliers.TryGetValue(affliction.Prefab.AfflictionType, out float vitalityTypeMultiplier))
{
multiplier *= vitalityTypeMultiplier;
}
return multiplier;
}
/// <summary>
/// How much vitality the affliction reduces, taking into account the effects of vitality modifiers on the limb the affliction is on (if limb-based)
/// </summary>
private float GetVitalityDecreaseWithVitalityMultipliers(Affliction affliction)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
if (afflictions.TryGetValue(affliction, out LimbHealth limbHealth) && limbHealth != null)
{
vitalityDecrease *= GetVitalityMultiplier(affliction, limbHealth);
}
return vitalityDecrease;
}
private void Kill()
{
if (Unkillable || Character.GodMode) { return; }
var (type, affliction) = GetCauseOfDeath();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
Character.Kill(type, affliction);
WasInFullHealth = false;
#if CLIENT
DisplayVitalityDelay = 0.0f;
DisplayedVitality = Vitality;
#endif
}
// We need to use another list of the afflictions when we call the status effects triggered by afflictions,
// because those status effects may add or remove other afflictions while iterating the collection.
private readonly List<Affliction> afflictionsCopy = [];
private bool isApplyingAfflictionStatusEffects;
public void ApplyAfflictionStatusEffects(ActionType type)
{
if (isApplyingAfflictionStatusEffects)
{
//pretty hacky: if we're already in the process of applying afflictions' status effects
//(i.e. calling this method caused some additional afflictions to appear and trigger status effects)
//let's instantiate a new list so we don't end up modifying afflictionsCopy while enumerating it
foreach (Affliction affliction in afflictions.Keys.ToList())
{
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
}
}
else
{
isApplyingAfflictionStatusEffects = true;
afflictionsCopy.Clear();
afflictionsCopy.AddRange(afflictions.Keys);
isApplyingAfflictionStatusEffects = true;
foreach (Affliction affliction in afflictionsCopy)
{
affliction.ApplyStatusEffects(type, 1.0f, this, targetLimb: GetAfflictionLimb(affliction));
}
isApplyingAfflictionStatusEffects = false;
}
}
public (CauseOfDeathType type, Affliction affliction) GetCauseOfDeath()
{
IEnumerable<Affliction> currentAfflictions = GetAllAfflictions(true);
Affliction strongestAffliction = null;
float largestStrength = 0.0f;
foreach (Affliction affliction in currentAfflictions)
{
if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
{
strongestAffliction = affliction;
largestStrength = affliction.GetVitalityDecrease(this);
}
}
CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
if (strongestAffliction == oxygenLowAffliction)
{
causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
}
return (causeOfDeath, strongestAffliction);
}
// Thread-static to avoid concurrent modification in parallel item updates
[ThreadStatic]
private static List<Affliction> t_allAfflictions;
private static List<Affliction> AllAfflictionsList => t_allAfflictions ??= new List<Affliction>();
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
var allAfflictions = AllAfflictionsList;
allAfflictions.Clear();
if (!mergeSameAfflictions)
{
allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
}
else
{
foreach (Affliction affliction in afflictions.Keys)
{
if (predicate != null && !predicate(affliction)) { continue; }
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
if (existingAffliction == null)
{
var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
if (affliction.Source != null) { newAffliction.Source = affliction.Source; }
newAffliction.DamagePerSecond = affliction.DamagePerSecond;
newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
allAfflictions.Add(newAffliction);
}
else
{
existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
existingAffliction.Strength += affliction.Strength;
}
}
}
return allAfflictions;
}
/// <summary>
/// Get the identifiers of the items that can be used to treat the character. Takes into account all the afflictions the character has,
/// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating)
/// </summary>
/// <param name="treatmentSuitability">A dictionary where the key is the identifier of the item and the value the suitability</param>
/// <param name="predictFutureDuration">If above 0, the method will take into account how much currently active status effects while affect the afflictions in the next x seconds.</param>
/// <param name="checkTreatmentThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentThreshold"/> (whether they're severe enough for AI to treat)?</param>
/// <param name="checkTreatmentSuggestionThreshold">Should the method check whether the afflictions are above <see cref="AfflictionPrefab.TreatmentSuggestionThreshold"/> (whether treatment suggestions are shown in the health interface)?</param>
public void GetSuitableTreatments(Dictionary<Identifier, float> treatmentSuitability, Character user, Limb limb = null, bool ignoreHiddenAfflictions = false,
bool checkTreatmentThreshold = true, bool checkTreatmentSuggestionThreshold = true,
float predictFutureDuration = 0.0f)
{
//key = item identifier
//float = suitability
treatmentSuitability.Clear();
float minSuitability = -10, maxSuitability = 10;
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (limb != null &&
affliction.Prefab.LimbSpecific &&
GetMatchingLimbHealth(affliction) != GetMatchingLimbHealth(limb))
{
if (limbHealth == null) { continue; }
int healthIndex = limbHealths.IndexOf(limbHealth);
if (limb.HealthIndex != healthIndex) { continue; }
}
float strength = affliction.Strength;
if (predictFutureDuration > 0.0f)
{
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
}
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
{
if (user == Character)
{
if (strength < affliction.Prefab.ShowIconThreshold) { continue; }
}
else
{
if (strength < affliction.Prefab.ShowIconToOthersThreshold) { continue; }
}
}
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitabilities)
{
float suitability = treatment.Value * strength;
if (suitability > 0)
{
//if this a suitable treatment, ignore it if the affliction isn't severe enough to treat
//if the suitability is negative though, we need to take it into account!
//otherwise we may end up e.g. giving too much opiates to someone already close to overdosing
if (checkTreatmentThreshold)
{
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (checkTreatmentSuggestionThreshold)
{
if (totalAfflictionStrength < affliction.Prefab.TreatmentSuggestionThreshold) { continue; }
}
}
if (treatment.Value > strength)
{
//avoid using very effective meds on small injuries
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
suitability /= overtreatmentFactor;
}
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = suitability;
}
else
{
treatmentSuitability[treatment.Key] += suitability;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
}
}
}
/// <summary>
/// Returns the total strength of instances of the same affliction on all the characters limbs,
/// with a smaller weight given to the other afflictions on other limbs
/// </summary>
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
{
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
if (affliction.Prefab.LimbSpecific)
{
foreach (Affliction otherAffliction in afflictions.Keys)
{
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
{
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
}
}
}
return totalAfflictionStrength;
}
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
afflictionTags.Clear();
foreach (Affliction affliction in afflictions.Keys)
{
var currentEffect = affliction.GetActiveEffect();
if (currentEffect is { Tag.IsEmpty: false })
{
afflictionTags.Add(currentEffect.Tag);
}
}
return afflictionTags;
}
public float GetPredictedStrength(Affliction affliction, float predictFutureDuration, Limb limb = null)
{
float strength = affliction.Strength;
foreach (var statusEffect in StatusEffect.DurationList)
{
if (!statusEffect.Targets.Any(t => t == Character || (limb != null && Character.AnimController.Limbs.Contains(t)))) { continue; }
float statusEffectDuration = Math.Min(statusEffect.Timer, predictFutureDuration);
foreach (var statusEffectAffliction in statusEffect.Parent.Afflictions)
{
if (statusEffectAffliction.Prefab == affliction.Prefab)
{
strength += statusEffectAffliction.Strength * statusEffectDuration;
}
}
foreach (var statusEffectAffliction in statusEffect.Parent.ReduceAffliction)
{
if (statusEffectAffliction.AfflictionIdentifier == affliction.Identifier ||
statusEffectAffliction.AfflictionIdentifier == affliction.Prefab.AfflictionType)
{
strength -= statusEffectAffliction.ReduceAmount * statusEffectDuration;
}
}
}
return MathHelper.Clamp(strength, 0.0f, affliction.Prefab.MaxStrength);
}
// Thread-static to avoid concurrent modification in parallel updates
[ThreadStatic]
private static List<Affliction> t_activeAfflictions;
[ThreadStatic]
private static List<(LimbHealth limbHealth, Affliction affliction)> t_limbAfflictions;
private static List<Affliction> ActiveAfflictionsList => t_activeAfflictions ??= new List<Affliction>();
private static List<(LimbHealth limbHealth, Affliction affliction)> LimbAfflictionsList => t_limbAfflictions ??= new List<(LimbHealth limbHealth, Affliction affliction)>();
public void ServerWrite(IWriteMessage msg)
{
var activeAfflictions = ActiveAfflictionsList;
activeAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (limbHealth != null) { continue; }
if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold)
{
activeAfflictions.Add(affliction);
}
}
msg.WriteByte((byte)activeAfflictions.Count);
foreach (Affliction affliction in activeAfflictions)
{
msg.WriteUInt32(affliction.Prefab.UintIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
{
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], 0, periodicEffect.MaxInterval, 8);
}
}
var limbAfflictions = LimbAfflictionsList;
limbAfflictions.Clear();
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var limbAffliction = kvp.Key;
var limbHealth = kvp.Value;
if (limbHealth == null) { continue; }
if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) { continue; }
limbAfflictions.Add((limbHealth, limbAffliction));
}
msg.WriteByte((byte)limbAfflictions.Count);
foreach (var (limbHealth, affliction) in limbAfflictions)
{
msg.WriteRangedInteger(limbHealths.IndexOf(limbHealth), 0, limbHealths.Count - 1);
msg.WriteUInt32(affliction.Prefab.UintIdentifier);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
msg.WriteByte((byte)affliction.Prefab.PeriodicEffects.Count);
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
{
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
}
}
}
public void Remove()
{
RemoveProjSpecific();
// Clear thread-static lists to help with garbage collection
AfflictionsToRemove.Clear();
AfflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength / a.Prefab.MaxStrength);
public void Save(XElement healthElement)
{
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
for (int i = 0; i < limbHealths.Count; i++)
{
var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
healthElement.Add(limbHealthElement);
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions.Where(a => a.Value == limbHealths[i]))
{
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f) { continue; }
limbHealthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
}
}
public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "affliction":
LoadAffliction(subElement);
break;
case "limbhealth":
int limbHealthIndex = subElement.GetAttributeInt("i", -1);
if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
continue;
}
foreach (XElement afflictionElement in subElement.Elements())
{
LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
}
break;
}
}
void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
{
string id = afflictionElement.GetAttributeString("identifier", "");
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return;
}
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.Keys.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else
{
afflictions.TryAdd(afflictionPrefab.Instantiate(strength), limbHealth);
}
}
}
}
}