Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
NotAlwaysTrue 59bc21973a OBT/1.2.0(Spring Update)
Sync with Upstream
2026-04-25 13:25:41 +08:00

1549 lines
69 KiB
C#

using Barotrauma.Abilities;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static OneOf.Types.TrueFalseOrNull;
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>();
private readonly Dictionary<Affliction, LimbHealth> afflictions = new Dictionary<Affliction, LimbHealth>();
private readonly HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
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.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
foreach (Affliction affliction in irremovableAfflictions)
{
afflictions.Add(affliction, null);
}
}
partial void InitProjSpecific(ContentXElement element, Character character);
public IReadOnlyCollection<Affliction> GetAllAfflictions()
{
return afflictions.Keys;
}
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)
{
lock (afflictions) {
// 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;
}
private readonly List<Affliction> matchingAfflictions = new List<Affliction>();
public void ReduceAllAfflictionsOnAllLimbs(float amount, ActionType? treatmentAction = null)
{
matchingAfflictions.Clear();
matchingAfflictions.AddRange(afflictions.Keys);
ReduceMatchingAfflictions(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"); }
matchingAfflictions.Clear();
foreach (var affliction in afflictions)
{
if (affliction.Key.Prefab.Identifier == afflictionIdOrType || affliction.Key.Prefab.AfflictionType == afflictionIdOrType)
{
matchingAfflictions.Add(affliction.Key);
}
}
ReduceMatchingAfflictions(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)); }
matchingAfflictions.Clear();
matchingAfflictions.AddRange(GetAfflictionsForLimb(targetLimb));
ReduceMatchingAfflictions(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)); }
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(amount, treatmentAction, attacker);
}
private void ReduceMatchingAfflictions(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;
}
bool? should = null;
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyDamage>(x => should = x.OnCharacterApplyDamage(this, attackResult, hitLimb, allowStacking) ?? should);
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();
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable || Character.GodMode) { return; }
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.Remove(affliction);
}
foreach (LimbHealth limbHealth in limbHealths)
{
if (damageAmount > 0.0f) { afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount), limbHealth); }
if (bleedingDamageAmount > 0.0f && DoesBleed) { afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount), limbHealth); }
if (burnDamageAmount > 0.0f) { afflictions.Add(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)
{
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(affliction => predicate(affliction)));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
}
CalculateVitality();
}
public void RemoveAllAfflictions()
{
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(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.Remove(affliction);
}
foreach (Affliction affliction in irremovableAfflictions)
{
affliction.Strength = 0.0f;
}
CalculateVitality();
}
public void RemoveNegativeAfflictions()
{
afflictionsToRemove.Clear();
afflictionsToRemove.AddRange(afflictions.Keys.Where(a =>
!irremovableAfflictions.Contains(a) &&
!a.Prefab.IsBuff &&
a.Prefab.AfflictionType != "geneticmaterialbuff" &&
a.Prefab.AfflictionType != "geneticmaterialdebuff"));
foreach (var affliction in afflictionsToRemove)
{
afflictions.Remove(affliction);
}
foreach (Affliction affliction in irremovableAfflictions)
{
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; }
bool? should = null;
LuaCsSetup.Instance.EventService.PublishEvent<IEventCharacterApplyAffliction>(x => should = x.OnCharacterApplyAffliction(this, limbHealth, newAffliction, allowStacking) ?? should);
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;
}
}
float modifiedStrength = newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab, limbType));
if (newAffliction.Prefab.AfflictionType == AfflictionPrefab.StunType)
{
//don't allow stunning for less than one frame
//fixes monsters/enemies that take some minuscule amount of stun from a weapon still being noticeable affected by the stun,
//because even a one-frame stun briefly disables the animations and makes the character stop
if (modifiedStrength < Timing.Step && Stun <= 0.0f)
{
return;
}
}
if (existingAffliction != null)
{
float newStrength = modifiedStrength;
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, modifiedStrength),
newAffliction.Source);
afflictions.Add(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)
{
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.Contains(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.Remove(affliction);
}
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);
}
private readonly List<Affliction> allAfflictions = new List<Affliction>();
private IEnumerable<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
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);
}
private readonly List<Affliction> activeAfflictions = new List<Affliction>();
private readonly List<(LimbHealth limbHealth, Affliction affliction)> limbAfflictions = new List<(LimbHealth limbHealth, Affliction affliction)>();
public void ServerWrite(IWriteMessage msg)
{
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);
}
}
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();
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.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else
{
afflictions.Add(afflictionPrefab.Instantiate(strength), limbHealth);
}
}
}
}
}